# HG changeset patch # User Alain Mazy # Date 1725290242 -7200 # Node ID 796cb17db15c8c658fed0c2fae4b442849e725ce # Parent 95a3802ad13326c1083bf31ca31c8ec9e3f9d81a# Parent 8bb3f2fca242cfe64ef323d2c0cd94adcbf4926b merged default -> find-refactoring diff -r 8bb3f2fca242 -r 796cb17db15c NEWS --- a/NEWS Thu Aug 29 13:46:49 2024 +0200 +++ b/NEWS Mon Sep 02 17:17:22 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 ----------- diff -r 8bb3f2fca242 -r 796cb17db15c OrthancFramework/Sources/DicomFormat/DicomArray.cpp --- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -95,7 +95,8 @@ } else if (v.IsSequence()) { - s = "(sequence)"; + //s = "(sequence)"; + s = "(sequence) " + v.GetSequenceContent().toStyledString(); } else { diff -r 8bb3f2fca242 -r 796cb17db15c OrthancFramework/Sources/SQLite/Connection.h --- a/OrthancFramework/Sources/SQLite/Connection.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/Connection.h Mon Sep 02 17:17:22 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 { diff -r 8bb3f2fca242 -r 796cb17db15c OrthancFramework/Sources/SQLite/StatementId.cpp --- a/OrthancFramework/Sources/SQLite/StatementId.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.cpp Mon Sep 02 17:17:22 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_; } } } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancFramework/Sources/SQLite/StatementId.h --- a/OrthancFramework/Sources/SQLite/StatementId.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.h Mon Sep 02 17:17:22 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; }; } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/CMakeLists.txt Mon Sep 02 17:17:22 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 diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Mon Sep 02 17:17:22 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" @@ -134,6 +135,136 @@ } + static void Convert(DatabasePluginMessages::DatabaseConstraint& target, + const DatabaseConstraint& source) + { + target.set_level(Convert(source.GetLevel())); + target.set_tag_group(source.GetTag().GetGroup()); + target.set_tag_element(source.GetTag().GetElement()); + target.set_is_identifier_tag(source.IsIdentifier()); + target.set_is_case_sensitive(source.IsCaseSensitive()); + target.set_is_mandatory(source.IsMandatory()); + + target.mutable_values()->Reserve(source.GetValuesCount()); + for (size_t j = 0; j < source.GetValuesCount(); j++) + { + target.add_values(source.GetValue(j)); + } + + switch (source.GetConstraintType()) + { + case ConstraintType_Equal: + target.set_type(DatabasePluginMessages::CONSTRAINT_EQUAL); + break; + + case ConstraintType_SmallerOrEqual: + target.set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL); + break; + + case ConstraintType_GreaterOrEqual: + target.set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL); + break; + + case ConstraintType_Wildcard: + target.set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD); + break; + + case ConstraintType_List: + target.set_type(DatabasePluginMessages::CONSTRAINT_LIST); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static DatabasePluginMessages::LabelsConstraintType Convert(LabelsConstraint constraint) + { + switch (constraint) + { + case LabelsConstraint_All: + return DatabasePluginMessages::LABELS_CONSTRAINT_ALL; + + case LabelsConstraint_Any: + return DatabasePluginMessages::LABELS_CONSTRAINT_ANY; + + case LabelsConstraint_None: + return DatabasePluginMessages::LABELS_CONSTRAINT_NONE; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static void Convert(DatabasePluginMessages::Find_Request_ChildrenSpecification& target, + const FindRequest::ChildrenSpecification& source) + { + target.set_retrieve_identifiers(source.IsRetrieveIdentifiers()); + + for (std::set::const_iterator it = source.GetMetadata().begin(); it != source.GetMetadata().end(); ++it) + { + target.add_retrieve_metadata(*it); + } + + for (std::set::const_iterator it = source.GetMainDicomTags().begin(); it != source.GetMainDicomTags().end(); ++it) + { + DatabasePluginMessages::Find_Request_Tag* tag = target.add_retrieve_main_dicom_tags(); + tag->set_group(it->GetGroup()); + tag->set_element(it->GetElement()); + } + } + + + static void Convert(FindResponse::Resource& target, + ResourceType level, + const DatabasePluginMessages::Find_Response_ResourceContent& source) + { + for (int i = 0; i < source.main_dicom_tags().size(); i++) + { + target.AddStringDicomTag(level, source.main_dicom_tags(i).group(), + source.main_dicom_tags(i).element(), source.main_dicom_tags(i).value()); + } + + for (int i = 0; i < source.metadata().size(); i++) + { + target.AddMetadata(level, static_cast(source.metadata(i).key()), source.metadata(i).value()); + } + } + + + static void Convert(FindResponse::Resource& target, + ResourceType level, + const DatabasePluginMessages::Find_Response_ChildrenContent& source) + { + for (int i = 0; i < source.identifiers().size(); i++) + { + target.AddChildIdentifier(level, source.identifiers(i)); + } + + for (int i = 0; i < source.main_dicom_tags().size(); i++) + { + const DicomTag tag(source.main_dicom_tags(i).group(), source.main_dicom_tags(i).element()); + + for (int j = 0; j < source.main_dicom_tags(i).values().size(); j++) + { + target.AddChildrenMainDicomTagValue(level, tag, source.main_dicom_tags(i).values(j)); + } + } + + for (int i = 0; i < source.metadata().size(); i++) + { + MetadataType key = static_cast(source.metadata(i).key()); + + for (int j = 0; j < source.metadata(i).values().size(); j++) + { + target.AddChildrenMetadataValue(level, key, source.metadata(i).values(j)); + } + } + } + + static void Execute(DatabasePluginMessages::Response& response, const OrthancPluginDatabaseV4& database, const DatabasePluginMessages::Request& request) @@ -972,7 +1103,7 @@ return response.is_disk_size_above().result(); } - + virtual void ApplyLookupResources(std::list& resourcesId, std::list* instancesId, // Can be NULL if not needed const DatabaseConstraints& lookup, @@ -996,47 +1127,7 @@ for (size_t i = 0; i < lookup.GetSize(); i++) { - const DatabaseConstraint& source = lookup.GetConstraint(i); - - DatabasePluginMessages::DatabaseConstraint* target = request.mutable_lookup_resources()->add_lookup(); - target->set_level(Convert(source.GetLevel())); - target->set_tag_group(source.GetTag().GetGroup()); - target->set_tag_element(source.GetTag().GetElement()); - target->set_is_identifier_tag(source.IsIdentifier()); - target->set_is_case_sensitive(source.IsCaseSensitive()); - target->set_is_mandatory(source.IsMandatory()); - - target->mutable_values()->Reserve(source.GetValuesCount()); - for (size_t j = 0; j < source.GetValuesCount(); j++) - { - target->add_values(source.GetValue(j)); - } - - switch (source.GetConstraintType()) - { - case ConstraintType_Equal: - target->set_type(DatabasePluginMessages::CONSTRAINT_EQUAL); - break; - - case ConstraintType_SmallerOrEqual: - target->set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL); - break; - - case ConstraintType_GreaterOrEqual: - target->set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL); - break; - - case ConstraintType_Wildcard: - target->set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD); - break; - - case ConstraintType_List: - target->set_type(DatabasePluginMessages::CONSTRAINT_LIST); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } + Convert(*request.mutable_lookup_resources()->add_lookup(), lookup.GetConstraint(i)); } for (std::set::const_iterator it = labels.begin(); it != labels.end(); ++it) @@ -1044,23 +1135,7 @@ request.mutable_lookup_resources()->add_labels(*it); } - switch (labelsConstraint) - { - case LabelsConstraint_All: - request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ALL); - break; - - case LabelsConstraint_Any: - request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ANY); - break; - - case LabelsConstraint_None: - request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_NONE); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } + request.mutable_lookup_resources()->set_labels_constraint(Convert(labelsConstraint)); DatabasePluginMessages::TransactionResponse response; ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES, request); @@ -1278,6 +1353,212 @@ { ListLabelsInternal(target, false, -1); } + + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE + { + if (capabilities.HasFindSupport()) + { + DatabasePluginMessages::TransactionRequest dbRequest; + dbRequest.mutable_find()->set_level(Convert(request.GetLevel())); + + if (request.GetOrthancIdentifiers().HasPatientId()) + { + dbRequest.mutable_find()->set_orthanc_id_patient(request.GetOrthancIdentifiers().GetPatientId()); + } + + if (request.GetOrthancIdentifiers().HasStudyId()) + { + dbRequest.mutable_find()->set_orthanc_id_study(request.GetOrthancIdentifiers().GetStudyId()); + } + + if (request.GetOrthancIdentifiers().HasSeriesId()) + { + dbRequest.mutable_find()->set_orthanc_id_series(request.GetOrthancIdentifiers().GetSeriesId()); + } + + if (request.GetOrthancIdentifiers().HasInstanceId()) + { + dbRequest.mutable_find()->set_orthanc_id_instance(request.GetOrthancIdentifiers().GetInstanceId()); + } + + for (size_t i = 0; i < request.GetDicomTagConstraints().GetSize(); i++) + { + Convert(*dbRequest.mutable_find()->add_dicom_tag_constraints(), request.GetDicomTagConstraints().GetConstraint(i)); + } + + if (request.HasLimits()) + { + dbRequest.mutable_find()->mutable_limits()->set_since(request.GetLimitsSince()); + dbRequest.mutable_find()->mutable_limits()->set_count(request.GetLimitsCount()); + } + + for (std::set::const_iterator it = request.GetLabels().begin(); it != request.GetLabels().end(); ++it) + { + dbRequest.mutable_find()->add_labels(*it); + } + + dbRequest.mutable_find()->set_labels_constraint(Convert(request.GetLabelsConstraint())); + + // TODO-FIND: ordering_ + // TODO-FIND: metadataConstraints__ + + dbRequest.mutable_find()->set_retrieve_main_dicom_tags(request.IsRetrieveMainDicomTags()); + dbRequest.mutable_find()->set_retrieve_metadata(request.IsRetrieveMetadata()); + dbRequest.mutable_find()->set_retrieve_labels(request.IsRetrieveLabels()); + dbRequest.mutable_find()->set_retrieve_attachments(request.IsRetrieveAttachments()); + dbRequest.mutable_find()->set_retrieve_parent_identifier(request.IsRetrieveParentIdentifier()); + dbRequest.mutable_find()->set_retrieve_at_least_one_instance(request.IsRetrieveOneInstanceIdentifier()); + + if (request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) + { + dbRequest.mutable_find()->mutable_parent_patient()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Patient).IsRetrieveMainDicomTags()); + dbRequest.mutable_find()->mutable_parent_patient()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Patient).IsRetrieveMetadata()); + } + + if (request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) + { + dbRequest.mutable_find()->mutable_parent_study()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Study).IsRetrieveMainDicomTags()); + dbRequest.mutable_find()->mutable_parent_study()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Study).IsRetrieveMetadata()); + } + + if (request.GetLevel() == ResourceType_Instance) + { + dbRequest.mutable_find()->mutable_parent_series()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Series).IsRetrieveMainDicomTags()); + dbRequest.mutable_find()->mutable_parent_series()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Series).IsRetrieveMetadata()); + } + + if (request.GetLevel() == ResourceType_Patient) + { + Convert(*dbRequest.mutable_find()->mutable_children_studies(), request.GetChildrenSpecification(ResourceType_Study)); + } + + if (request.GetLevel() == ResourceType_Patient || + request.GetLevel() == ResourceType_Study) + { + Convert(*dbRequest.mutable_find()->mutable_children_series(), request.GetChildrenSpecification(ResourceType_Series)); + } + + if (request.GetLevel() == ResourceType_Patient || + request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series) + { + Convert(*dbRequest.mutable_find()->mutable_children_instances(), request.GetChildrenSpecification(ResourceType_Instance)); + } + + DatabasePluginMessages::TransactionResponse dbResponse; + ExecuteTransaction(dbResponse, DatabasePluginMessages::OPERATION_FIND, dbRequest); + + for (int i = 0; i < dbResponse.find().size(); i++) + { + const DatabasePluginMessages::Find_Response& source = dbResponse.find(i); + + std::unique_ptr target( + new FindResponse::Resource(request.GetLevel(), source.internal_id(), source.public_id())); + + if (request.IsRetrieveParentIdentifier()) + { + target->SetParentIdentifier(source.parent_public_id()); + } + + for (int i = 0; i < source.labels().size(); i++) + { + target->AddLabel(source.labels(i)); + } + + for (int i = 0; i < source.attachments().size(); i++) + { + target->AddAttachment(Convert(source.attachments(i))); + } + + Convert(*target, ResourceType_Patient, source.patient_content()); + + if (request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) + { + Convert(*target, ResourceType_Study, source.study_content()); + } + + if (request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) + { + Convert(*target, ResourceType_Series, source.series_content()); + } + + if (request.GetLevel() == ResourceType_Instance) + { + Convert(*target, ResourceType_Instance, source.instance_content()); + } + + if (request.GetLevel() == ResourceType_Patient) + { + Convert(*target, ResourceType_Patient, source.children_studies_content()); + } + + if (request.GetLevel() == ResourceType_Patient || + request.GetLevel() == ResourceType_Study) + { + Convert(*target, ResourceType_Study, source.children_series_content()); + } + + if (request.GetLevel() == ResourceType_Patient || + request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series) + { + Convert(*target, ResourceType_Series, source.children_instances_content()); + } + + response.Add(target.release()); + } + + throw OrthancException(ErrorCode_NotImplemented); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + virtual void ExecuteFind(std::list& identifiers, + const Capabilities& capabilities, + const FindRequest& request) ORTHANC_OVERRIDE + { + if (capabilities.HasFindSupport()) + { + // The integrated version of "ExecuteFind()" should have been called + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Compatibility::GenericFind find(*this); + find.ExecuteFind(identifiers, capabilities, request); + } + } + + + virtual void ExecuteExpand(FindResponse& response, + const Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) ORTHANC_OVERRIDE + { + if (capabilities.HasFindSupport()) + { + // The integrated version of "ExecuteFind()" should have been called + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Compatibility::GenericFind find(*this); + find.ExecuteExpand(response, capabilities, request, identifier); + } + } }; @@ -1366,6 +1647,9 @@ dbCapabilities_.SetAtomicIncrementGlobalProperty(systemInfo.supports_increment_global_property()); dbCapabilities_.SetUpdateAndGetStatistics(systemInfo.has_update_and_get_statistics()); dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency()); + dbCapabilities_.SetHasFindSupport(systemInfo.supports_find()); + + printf(">>> %d\n", dbCapabilities_.HasFindSupport()); } open_ = true; @@ -1492,4 +1776,10 @@ return dbCapabilities_; } } + + + bool OrthancPluginDatabaseV4::HasIntegratedFind() const + { + return dbCapabilities_.HasFindSupport(); + } } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Mon Sep 02 17:17:22 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; }; } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Mon Sep 02 17:17:22 2024 +0200 @@ -121,7 +121,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 12 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 4 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 5 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto --- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Mon Sep 02 17:17:22 2024 +0200 @@ -141,6 +141,7 @@ bool supports_increment_global_property = 5; bool has_update_and_get_statistics = 6; bool has_measure_latency = 7; + bool supports_find = 8; // New in Orthanc 1.12.5 } } @@ -288,11 +289,12 @@ OPERATION_GET_CHILDREN_METADATA = 42; OPERATION_GET_LAST_CHANGE_INDEX = 43; OPERATION_LOOKUP_RESOURCE_AND_PARENT = 44; - OPERATION_ADD_LABEL = 45; // New in Orthanc 1.12.0 - OPERATION_REMOVE_LABEL = 46; // New in Orthanc 1.12.0 - OPERATION_LIST_LABELS = 47; // New in Orthanc 1.12.0 - OPERATION_INCREMENT_GLOBAL_PROPERTY = 48; // New in Orthanc 1.12.3 - OPERATION_UPDATE_AND_GET_STATISTICS = 49; // New in Orthanc 1.12.3 + OPERATION_ADD_LABEL = 45; // New in Orthanc 1.12.0 + OPERATION_REMOVE_LABEL = 46; // New in Orthanc 1.12.0 + OPERATION_LIST_LABELS = 47; // New in Orthanc 1.12.0 + OPERATION_INCREMENT_GLOBAL_PROPERTY = 48; // New in Orthanc 1.12.3 + OPERATION_UPDATE_AND_GET_STATISTICS = 49; // New in Orthanc 1.12.3 + OPERATION_FIND = 50; // New in Orthanc 1.12.5 } message Rollback { @@ -824,6 +826,99 @@ } } +message Find { // New in Orthanc 1.12.5 + message Request { // This corresponds to "FindRequest" in C++ + message Tag { + uint32 group = 1; + uint32 element = 2; + } + message Limits { + uint64 since = 1; + uint64 count = 2; + } + message ParentSpecification { + bool retrieve_main_dicom_tags = 1; + bool retrieve_metadata = 2; + } + message ChildrenSpecification { + bool retrieve_identifiers = 1; + repeated int32 retrieve_metadata = 2; + repeated Tag retrieve_main_dicom_tags = 3; + } + + // Part 1 of the request: Constraints + ResourceType level = 1; + string orthanc_id_patient = 2; // optional - GetOrthancIdentifiers().GetPatientId(); + string orthanc_id_study = 3; // optional - GetOrthancIdentifiers().GetStudyId(); + string orthanc_id_series = 4; // optional - GetOrthancIdentifiers().GetSeriesId(); + string orthanc_id_instance = 5; // optional - GetOrthancIdentifiers().GetInstanceId(); + repeated DatabaseConstraint dicom_tag_constraints = 6; + Limits limits = 7; // optional + repeated string labels = 8; + LabelsConstraintType labels_constraint = 9; + + // TODO-FIND: ordering_ + // TODO-FIND: metadataConstraints_ + + // Part 2 of the request: What is to be retrieved + bool retrieve_main_dicom_tags = 100; + bool retrieve_metadata = 101; + bool retrieve_labels = 102; + bool retrieve_attachments = 103; + bool retrieve_parent_identifier = 104; + bool retrieve_at_least_one_instance = 105; + ParentSpecification parent_patient = 106; + ParentSpecification parent_study = 107; + ParentSpecification parent_series = 108; + ChildrenSpecification children_studies = 109; + ChildrenSpecification children_series = 110; + ChildrenSpecification children_instances = 111; + } + + message Response { // This corresponds to "FindResponse" in C++ + message Tag { + uint32 group = 1; + uint32 element = 2; + string value = 3; + } + message Metadata { + int32 key = 1; + string value = 2; + } + message MultipleTags { + uint32 group = 1; + uint32 element = 2; + repeated string values = 3; + } + message MultipleMetadata { + int32 key = 1; + repeated string values = 2; + } + message ResourceContent { + repeated Tag main_dicom_tags = 1; + repeated Metadata metadata = 2; + } + message ChildrenContent { + repeated string identifiers = 1; + repeated MultipleTags main_dicom_tags = 2; + repeated MultipleMetadata metadata = 3; + } + + int64 internal_id = 1; + string public_id = 2; + string parent_public_id = 3; // optional + repeated string labels = 4; + repeated FileInfo attachments = 5; + ResourceContent patient_content = 6; + ResourceContent study_content = 7; + ResourceContent series_content = 8; + ResourceContent instance_content = 9; + ChildrenContent children_studies_content = 10; + ChildrenContent children_series_content = 11; + ChildrenContent children_instances_content = 12; + } +} + message TransactionRequest { sfixed64 transaction = 1; TransactionOperation operation = 2; @@ -878,6 +973,7 @@ ListLabels.Request list_labels = 147; IncrementGlobalProperty.Request increment_global_property = 148; UpdateAndGetStatistics.Request update_and_get_statistics = 149; + Find.Request find = 150; } message TransactionResponse { @@ -931,6 +1027,7 @@ ListLabels.Response list_labels = 147; IncrementGlobalProperty.Response increment_global_property = 148; UpdateAndGetStatistics.Response update_and_get_statistics = 149; + repeated Find.Response find = 150; // One message per found resources } enum RequestType { diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Resources/Graveyard/FindRefactoringForSQLite.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/Graveyard/FindRefactoringForSQLite.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -0,0 +1,167 @@ +#if 0 + // TODO-FIND: Remove this implementation, as it should be done by + // the compatibility mode implemented by "GenericFind" + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector& normalized) ORTHANC_OVERRIDE + { +#if 0 + Compatibility::GenericFind find(*this); + find.Execute(response, request); +#else + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS FilteredResourcesIds"); + s.Run(); + } + + { + + LookupFormatter formatter; + + std::string sqlLookup; + LookupFormatter::Apply(sqlLookup, + formatter, + normalized, + request.GetLevel(), + request.GetLabels(), + request.GetLabelsConstraint(), + (request.HasLimits() ? request.GetLimitsCount() : 0)); // TODO: handles since and count + + { + // first create a temporary table that with the filtered and ordered results + sqlLookup = "CREATE TEMPORARY TABLE FilteredResourcesIds AS " + sqlLookup; + + SQLite::Statement statement(db_, SQLITE_FROM_HERE_DYNAMIC(sqlLookup), sqlLookup); + formatter.Bind(statement); + statement.Run(); + } + + { + // create the response item with the public ids only + SQLite::Statement statement(db_, SQLITE_FROM_HERE, "SELECT publicId FROM FilteredResourcesIds"); + formatter.Bind(statement); + + while (statement.Step()) + { + const std::string resourceId = statement.ColumnString(0); + response.Add(new FindResponse::Resource(request.GetLevel(), resourceId)); + } + } + + // request Each response content through INNER JOIN with the temporary table + if (request.IsRetrieveMainDicomTags()) + { + // TODO-FIND: handle the case where we request tags from multiple levels + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT publicId, tagGroup, tagElement, value FROM MainDicomTags AS tags " + " INNER JOIN FilteredResourcesIds ON tags.id = FilteredResourcesIds.internalId"); + formatter.Bind(statement); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddStringDicomTag(statement.ColumnInt(1), + statement.ColumnInt(2), + statement.ColumnString(3)); + } + } + + if (request.IsRetrieveChildrenIdentifiers()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, childLevel.publicId AS childPublicId " + "FROM Resources as currentLevel " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId " + " INNER JOIN Resources childLevel ON childLevel.parentId = currentLevel.internalId"); + formatter.Bind(statement); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddChildIdentifier(GetChildResourceType(request.GetLevel()), statement.ColumnString(1)); + } + } + + if (request.IsRetrieveParentIdentifier()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, parentLevel.publicId AS parentPublicId " + "FROM Resources as currentLevel " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId " + " INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + const std::string& parentId = statement.ColumnString(1); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).SetParentIdentifier(parentId); + } + } + + if (request.IsRetrieveMetadata()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, metadata.type, metadata.value " + "FROM Metadata " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Metadata.id"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddMetadata(static_cast(statement.ColumnInt(1)), + statement.ColumnString(2)); + } + } + + if (request.IsRetrieveLabels()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, label " + "FROM Labels " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Labels.id"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddLabel(statement.ColumnString(1)); + } + } + + if (request.IsRetrieveAttachments()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, uuid, fileType, uncompressedSize, compressionType, compressedSize, " + " uncompressedMD5, compressedMD5 " + "FROM AttachedFiles " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = AttachedFiles.id"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + FileInfo attachment = FileInfo(statement.ColumnString(1), + static_cast(statement.ColumnInt(2)), + statement.ColumnInt64(3), + statement.ColumnString(6), + static_cast(statement.ColumnInt(4)), + statement.ColumnInt64(5), + statement.ColumnString(7)); + + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddAttachment(attachment); + }; + } + + // TODO-FIND: implement other responseContent: ResponseContent_ChildInstanceId, ResponseContent_ChildrenMetadata (later: ResponseContent_IsStable) + + } + +#endif + } +#endif + diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Resources/Orthanc.doxygen --- a/OrthancServer/Resources/Orthanc.doxygen Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Resources/Orthanc.doxygen Mon Sep 02 17:17:22 2024 +0200 @@ -755,6 +755,7 @@ # Note: If this tag is empty the current directory is searched. INPUT = @CMAKE_SOURCE_DIR@/../OrthancFramework/Sources \ + @CMAKE_SOURCE_DIR@/Plugins/Engine \ @CMAKE_SOURCE_DIR@/Sources # This tag can be used to specify the character encoding of the source files diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Resources/RunCppCheck.sh --- a/OrthancServer/Resources/RunCppCheck.sh Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Mon Sep 02 17:17:22 2024 +0200 @@ -16,8 +16,8 @@ stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1477 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74 -stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:374 -stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:378 +stlFindInsert:../../OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp:65 +stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:480 stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:41 stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:191 stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:361 @@ -34,9 +34,9 @@ useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:277 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026 -assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:290 -assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:389 -assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3663 +assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292 +assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391 +assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3527 assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286 assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454 EOF diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -24,6 +24,7 @@ #include "BaseDatabaseWrapper.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "Compatibility/GenericFind.h" namespace Orthanc { @@ -46,6 +47,33 @@ } + void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + + void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(std::list& identifiers, + const Capabilities& capabilities, + const FindRequest& request) + { + Compatibility::GenericFind find(*this); + find.ExecuteFind(identifiers, capabilities, request); + } + + + void BaseDatabaseWrapper::BaseTransaction::ExecuteExpand(FindResponse& response, + const Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) + { + Compatibility::GenericFind find(*this); + find.ExecuteExpand(response, capabilities, request, identifier); + } + + uint64_t BaseDatabaseWrapper::MeasureLatency() { throw OrthancException(ErrorCode_NotImplemented); // only implemented in V4 diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/BaseDatabaseWrapper.h --- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Mon Sep 02 17:17:22 2024 +0200 @@ -47,8 +47,26 @@ 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& identifiers, + const Capabilities& capabilities, + const FindRequest& request) ORTHANC_OVERRIDE; + + virtual void ExecuteExpand(FindResponse& response, + const Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) ORTHANC_OVERRIDE; }; virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; + + virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE + { + return false; + } }; } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/Compatibility/GenericFind.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -0,0 +1,596 @@ +/** + * 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 . + **/ + + +#include "GenericFind.h" + +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../../OrthancFramework/Sources/OrthancException.h" + +#include + + +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& target, + IDatabaseWrapper::ITransaction& transaction, + const std::list& resources) + { + target.clear(); + + for (std::list::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + std::list tmp; + transaction.GetChildrenInternalId(tmp, *it); + target.splice(target.begin(), tmp); + } + } + + static void GetChildren(std::list& target, + IDatabaseWrapper::ITransaction& transaction, + const std::list& resources) + { + target.clear(); + + for (std::list::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + std::list tmp; + transaction.GetChildrenPublicId(tmp, *it); + target.splice(target.begin(), tmp); + } + } + + static void GetChildrenIdentifiers(std::list& children, + IDatabaseWrapper::ITransaction& transaction, + const OrthancIdentifiers& identifiers, + ResourceType topLevel, + ResourceType bottomLevel) + { + if (!IsResourceLevelAboveOrEqual(topLevel, bottomLevel) || + topLevel == bottomLevel) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::list 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 nextResources; + GetChildren(nextResources, transaction, currentResources); + currentResources.swap(nextResources); + } + + currentLevel = nextLevel; + } + } + + void GenericFind::ExecuteFind(std::list& identifiers, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request) + { + 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() && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + 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 IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) + { + int64_t internalId; + ResourceType level; + std::string parent; + + if (request.IsRetrieveParentIdentifier()) + { + if (!transaction_.LookupResourceAndParent(internalId, level, parent, identifier)) + { + return; // The resource is not available anymore + } + + if (level == ResourceType_Patient) + { + if (!parent.empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + else + { + if (parent.empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + else + { + if (!transaction_.LookupResource(internalId, level, identifier)) + { + return; // The resource is not available anymore + } + } + + if (level != request.GetLevel()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + std::unique_ptr resource(new FindResponse::Resource(request.GetLevel(), 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 (capabilities.HasLabelsSupport() && + request.IsRetrieveLabels()) + { + transaction_.ListLabels(resource->GetLabels(), internalId); + } + + if (request.IsRetrieveAttachments()) + { + std::set attachments; + transaction_.ListAvailableAttachments(attachments, internalId); + + for (std::set::const_iterator it = attachments.begin(); it != attachments.end(); ++it) + { + FileInfo info; + int64_t revision; + if (transaction_.LookupAttachment(info, revision, internalId, *it) && + info.GetContentType() == *it) + { + resource->AddAttachment(info); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + + { + const ResourceType bottomLevel = GetBottomLevelOfInterest(request); + + std::list currentIds; + currentIds.push_back(internalId); + + ResourceType currentLevel = level; + + while (currentLevel != bottomLevel) + { + ResourceType childrenLevel = GetChildResourceType(currentLevel); + + if (request.GetChildrenSpecification(childrenLevel).IsRetrieveIdentifiers()) + { + for (std::list::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it) + { + std::list ids; + transaction_.GetChildrenPublicId(ids, *it); + + for (std::list::const_iterator it2 = ids.begin(); it2 != ids.end(); ++it2) + { + resource->AddChildIdentifier(childrenLevel, *it2); + } + } + } + + const std::set& metadata = request.GetChildrenSpecification(childrenLevel).GetMetadata(); + + for (std::set::const_iterator it = metadata.begin(); it != metadata.end(); ++it) + { + for (std::list::const_iterator it2 = currentIds.begin(); it2 != currentIds.end(); ++it2) + { + std::list values; + transaction_.GetChildrenMetadata(values, *it2, *it); + + for (std::list::const_iterator it3 = values.begin(); it3 != values.end(); ++it3) + { + resource->AddChildrenMetadataValue(childrenLevel, *it, *it3); + } + } + } + + const std::set& mainDicomTags = request.GetChildrenSpecification(childrenLevel).GetMainDicomTags(); + + if (childrenLevel != bottomLevel || + !mainDicomTags.empty()) + { + std::list childrenIds; + + for (std::list::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it) + { + std::list tmp; + transaction_.GetChildrenInternalId(tmp, *it); + + childrenIds.splice(childrenIds.end(), tmp); + } + + if (!mainDicomTags.empty()) + { + for (std::list::const_iterator it = childrenIds.begin(); it != childrenIds.end(); ++it) + { + DicomMap m; + transaction_.GetMainDicomTags(m, *it); + + for (std::set::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 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()); + } + } +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/Compatibility/GenericFind.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h Mon Sep 02 17:17:22 2024 +0200 @@ -0,0 +1,57 @@ +/** + * 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 . + **/ + + +#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& identifiers, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request); + + void ExecuteExpand(FindResponse& response, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier); + }; + } +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/FindRequest.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -0,0 +1,260 @@ +/** + * 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 . + **/ + + +#include "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + +#include "MainDicomTagsRegistry.h" + +#include + + +namespace Orthanc +{ + FindRequest::ParentSpecification& FindRequest::GetParentSpecification(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_) || + 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) || + 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::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; + } + } +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/FindRequest.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.h Mon Sep 02 17:17:22 2024 +0200 @@ -0,0 +1,425 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h" +#include "../Search/DatabaseConstraint.h" +#include "../Search/DicomTagConstraint.h" +#include "../Search/ISqlLookupFormatter.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" + +#include +#include +#include +#include +#include + +namespace Orthanc +{ + class MainDicomTagsRegistry; + + class FindRequest : public boost::noncopyable + { + public: + 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 metadata_; + std::set 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& GetMetadata() const + { + return metadata_; + } + + void AddMainDicomTag(const DicomTag& tag) + { + mainDicomTags_.insert(tag); + } + + const std::set& 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) + bool hasLimits_; + uint64_t limitsSince_; + uint64_t limitsCount_; + std::set labels_; + LabelsConstraint labelsConstraint_; + + // TODO-FIND + std::deque ordering_; // The ordering criteria (note: the order is important !) + std::deque /* TODO-FIND */ metadataConstraints_; // All metadata filters (note: the order is not 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_; + + 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 ClearLimits() + { + hasLimits_ = false; + } + + void SetLimits(uint64_t since, + uint64_t count); + + bool HasLimits() const + { + return hasLimits_; + } + + uint64_t GetLimitsSince() const; + + uint64_t GetLimitsCount() const; + + void AddOrdering(const DicomTag& tag, + OrderingDirection direction); + + void AddOrdering(MetadataType metadataType, + OrderingDirection direction); + + const std::deque& GetOrdering() const + { + return ordering_; + } + + void SetLabels(const std::set& labels) + { + labels_ = labels; + } + + void AddLabel(const std::string& label) + { + labels_.insert(label); + } + + const std::set& 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(*this).GetParentSpecification(level); + } + + ChildrenSpecification& GetChildrenSpecification(ResourceType level); + + const ChildrenSpecification& GetChildrenSpecification(ResourceType level) const + { + return const_cast(*this).GetChildrenSpecification(level); + } + + void SetRetrieveOneInstanceIdentifier(bool retrieve); + + bool IsRetrieveOneInstanceIdentifier() const + { + return (retrieveOneInstanceIdentifier_ || + (level_ != ResourceType_Instance && + GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers())); + } + }; +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/FindResponse.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.cpp Mon Sep 02 17:17:22 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 . + **/ + + +#include "FindResponse.h" + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../OrthancFramework/Sources/SerializationToolbox.h" + +#include +#include + + +namespace Orthanc +{ + class FindResponse::MainDicomTagsAtLevel::DicomValue : public boost::noncopyable + { + public: + enum ValueType + { + ValueType_String, + ValueType_Null + }; + + private: + ValueType type_; + std::string value_; + + public: + DicomValue(ValueType type, + const std::string& value) : + type_(type), + value_(value) + { + } + + ValueType GetType() const + { + return type_; + } + + const std::string& GetValue() const + { + switch (type_) + { + case ValueType_Null: + throw OrthancException(ErrorCode_BadSequenceOfCalls); + + case ValueType_String: + return value_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + }; + + + FindResponse::MainDicomTagsAtLevel::~MainDicomTagsAtLevel() + { + for (MainDicomTags::iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void FindResponse::MainDicomTagsAtLevel::AddNullDicomTag(uint16_t group, + uint16_t element) + { + const DicomTag tag(group, element); + + if (mainDicomTags_.find(tag) == mainDicomTags_.end()) + { + mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_Null, ""); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::MainDicomTagsAtLevel::AddStringDicomTag(uint16_t group, + uint16_t element, + const std::string& value) + { + const DicomTag tag(group, element); + + if (mainDicomTags_.find(tag) == mainDicomTags_.end()) + { + mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_String, value); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::MainDicomTagsAtLevel::Export(DicomMap& target) const + { + for (MainDicomTags::const_iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it) + { + assert(it->second != NULL); + + switch (it->second->GetType()) + { + case DicomValue::ValueType_String: + target.SetValue(it->first, it->second->GetValue(), false /* not binary */); + break; + + case DicomValue::ValueType_Null: + target.SetNullValue(it->first); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + 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 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& 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 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& 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& m = GetMetadata(level); + + if (m.find(metadata) != m.end()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // Metadata already present + } + else + { + m[metadata] = value; + } + } + + + std::map& FindResponse::Resource::GetMetadata(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + switch (level) + { + case ResourceType_Patient: + return metadataPatient_; + + case ResourceType_Study: + return metadataStudy_; + + case ResourceType_Series: + return metadataSeries_; + + case ResourceType_Instance: + return metadataInstance_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool FindResponse::Resource::LookupMetadata(std::string& value, + ResourceType level, + MetadataType metadata) const + { + const std::map& m = GetMetadata(level); + + std::map::const_iterator found = m.find(metadata); + + if (found == m.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + 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::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& 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& m) + { + target = Json::objectValue; + + for (std::map::const_iterator it = m.begin(); it != m.end(); ++it) + { + target[EnumerationToString(it->first)] = it->second; + } + } + + + static void DebugAddAttachment(Json::Value& target, + const FileInfo& info) + { + Json::Value u = Json::arrayValue; + u.append(info.GetUuid()); + u.append(static_cast(info.GetUncompressedSize())); + target[EnumerationToString(info.GetContentType())] = u; + } + + + static void DebugSetOfStrings(Json::Value& target, + const std::set& values) + { + target = Json::arrayValue; + for (std::set::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]).IsRetrieveMetadata()) + { + 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& metadata = request.GetChildrenSpecification(levels[i]).GetMetadata(); + for (std::set::const_iterator it = metadata.begin(); it != metadata.end(); ++it) + { + std::set values; + GetChildrenInformation(levels[i]).GetMetadataValues(values, *it); + DebugSetOfStrings(target[level]["Metadata"][EnumerationToString(*it)], values); + } + + const std::set& tags = request.GetChildrenSpecification(levels[i]).GetMainDicomTags(); + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + std::set 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::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 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; + } + } +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/FindResponse.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.h Mon Sep 02 17:17:22 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 . + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" +#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" +#include "FindRequest.h" + +#include +#include +#include +#include +#include + + +namespace Orthanc +{ + class FindResponse : public boost::noncopyable + { + private: + class MainDicomTagsAtLevel : public boost::noncopyable + { + private: + class DicomValue; + + typedef std::map MainDicomTags; + + MainDicomTags mainDicomTags_; + + public: + ~MainDicomTagsAtLevel(); + + void AddStringDicomTag(uint16_t group, + uint16_t element, + const std::string& value); + + // The "Null" value could be used in the future to indicate a + // value that is not available, typically a new "ExtraMainDicomTag" + void AddNullDicomTag(uint16_t group, + uint16_t element); + + void Export(DicomMap& target) const; + }; + + class ChildrenInformation : public boost::noncopyable + { + private: + typedef std::map* > MetadataValues; + typedef std::map* > MainDicomTagValues; + + std::set identifiers_; + MetadataValues metadataValues_; + MainDicomTagValues mainDicomTagValues_; + + public: + ~ChildrenInformation(); + + void AddIdentifier(const std::string& identifier); + + const std::set& GetIdentifiers() const + { + return identifiers_; + } + + void AddMetadataValue(MetadataType metadata, + const std::string& value); + + void GetMetadataValues(std::set& values, + MetadataType metadata) const; + + void AddMainDicomTagValue(const DicomTag& tag, + const std::string& value); + + void GetMainDicomTagValues(std::set& values, + const DicomTag& tag) const; + }; + + + public: + class Resource : public boost::noncopyable + { + private: + typedef std::map*> ChildrenMetadata; + + ResourceType level_; + int64_t internalId_; // Internal ID of the resource in the database + std::string identifier_; + std::unique_ptr parentIdentifier_; + MainDicomTagsAtLevel mainDicomTagsPatient_; + MainDicomTagsAtLevel mainDicomTagsStudy_; + MainDicomTagsAtLevel mainDicomTagsSeries_; + MainDicomTagsAtLevel mainDicomTagsInstance_; + std::map metadataPatient_; + std::map metadataStudy_; + std::map metadataSeries_; + std::map metadataInstance_; + ChildrenInformation childrenStudiesInformation_; + ChildrenInformation childrenSeriesInformation_; + ChildrenInformation childrenInstancesInformation_; + std::set labels_; + std::map attachments_; + + MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level); + + const MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level) const + { + return const_cast(*this).GetMainDicomTagsAtLevel(level); + } + + ChildrenInformation& GetChildrenInformation(ResourceType level); + + const ChildrenInformation& GetChildrenInformation(ResourceType level) const + { + return const_cast(*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& GetMetadata(ResourceType level); + + const std::map& GetMetadata(ResourceType level) const + { + return const_cast(*this).GetMetadata(level); + } + + bool LookupMetadata(std::string& value, + ResourceType level, + MetadataType metadata) const; + + void AddChildIdentifier(ResourceType level, + const std::string& childId) + { + GetChildrenInformation(level).AddIdentifier(childId); + } + + const std::set& 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& 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& values, + ResourceType level, + const DicomTag& tag) const + { + GetChildrenInformation(level).GetMainDicomTagValues(values, tag); + } + + void AddLabel(const std::string& label); + + std::set& GetLabels() + { + return labels_; + } + + const std::set& GetLabels() const + { + return labels_; + } + + void AddAttachment(const FileInfo& attachment); + + bool LookupAttachment(FileInfo& target, + FileContentType type) const; + + const std::map& GetAttachments() const + { + return attachments_; + } + + 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 Index; + + std::deque 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(*this).GetResourceByIdentifier(id); + } + + bool HasResource(const std::string& id) const + { + return (index_.find(id) != index_.end()); + } + }; +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/IDatabaseWrapper.h --- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Mon Sep 02 17:17:22 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 @@ -52,6 +54,7 @@ bool hasAtomicIncrementGlobalProperty_; bool hasUpdateAndGetStatistics_; bool hasMeasureLatency_; + bool hasFindSupport_; public: Capabilities() : @@ -60,7 +63,8 @@ hasLabelsSupport_(false), hasAtomicIncrementGlobalProperty_(false), hasUpdateAndGetStatistics_(false), - hasMeasureLatency_(false) + hasMeasureLatency_(false), + hasFindSupport_(false) { } @@ -123,6 +127,16 @@ { return hasMeasureLatency_; } + + void SetHasFindSupport(bool value) + { + hasFindSupport_ = value; + } + + bool HasFindSupport() const + { + return hasFindSupport_; + } }; @@ -351,6 +365,34 @@ 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& identifiers, + const Capabilities& capabilities, + const FindRequest& request) = 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 Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) = 0; }; @@ -375,5 +417,9 @@ virtual const Capabilities GetDatabaseCapabilities() const = 0; virtual uint64_t MeasureLatency() = 0; + + // Returns "true" iff. the database engine supports the + // simultaneous find and expansion of resources. + virtual bool HasIntegratedFind() const = 0; }; } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -0,0 +1,142 @@ +/** + * 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 . + **/ + + +#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 tags; + DicomMap::GetMainDicomTags(tags, level); + + for (std::set::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(); + } + } + + + bool MainDicomTagsRegistry::NormalizeLookup(DatabaseConstraints& target, + const DatabaseLookup& source, + ResourceType queryLevel) const + { + bool isEquivalentLookup = true; + + 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; + } + + bool isEquivalentConstraint; + target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(isEquivalentConstraint, level, type)); + + if (!isEquivalentConstraint) + { + isEquivalentLookup = false; + } + } + else + { + isEquivalentLookup = false; + } + } + + return isEquivalentLookup; + } +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/MainDicomTagsRegistry.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h Mon Sep 02 17:17:22 2024 +0200 @@ -0,0 +1,88 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "../Search/DatabaseLookup.h" + +#include + + +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 Registry; + + Registry registry_; + + void LoadTags(ResourceType level); + + public: + MainDicomTagsRegistry(); + + void LookupTag(ResourceType& level, + DicomTagType& type, + const DicomTag& tag) const; + + /** + * Returns "true" iff. the normalized lookup is the same as the + * original DatabaseLookup. If "false" is returned, the target + * constraints are less strict than the original DatabaseLookup, + * so more resources will match them. + **/ + bool NormalizeLookup(DatabaseConstraints& target, + const DatabaseLookup& source, + ResourceType queryLevel) const; + }; +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/OrthancIdentifiers.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp Mon Sep 02 17:17:22 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 . + **/ + + +#include "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + + +namespace Orthanc +{ + OrthancIdentifiers::OrthancIdentifiers(const OrthancIdentifiers& other) + { + if (other.HasPatientId()) + { + SetPatientId(other.GetPatientId()); + } + + if (other.HasStudyId()) + { + SetStudyId(other.GetStudyId()); + } + + if (other.HasSeriesId()) + { + SetSeriesId(other.GetSeriesId()); + } + + if (other.HasInstanceId()) + { + SetInstanceId(other.GetInstanceId()); + } + } + + + void OrthancIdentifiers::SetPatientId(const std::string& id) + { + if (HasPatientId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + patientId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetPatientId() const + { + if (HasPatientId()) + { + return *patientId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetStudyId(const std::string& id) + { + if (HasStudyId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + studyId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetStudyId() const + { + if (HasStudyId()) + { + return *studyId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetSeriesId(const std::string& id) + { + if (HasSeriesId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + seriesId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetSeriesId() const + { + if (HasSeriesId()) + { + return *seriesId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetInstanceId(const std::string& id) + { + if (HasInstanceId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + instanceId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetInstanceId() const + { + if (HasInstanceId()) + { + return *instanceId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + ResourceType OrthancIdentifiers::DetectLevel() const + { + if (HasPatientId() && + !HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Patient; + } + else if (HasPatientId() && + HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Study; + } + else if (HasPatientId() && + HasStudyId() && + HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Series; + } + else if (HasPatientId() && + HasStudyId() && + HasSeriesId() && + HasInstanceId()) + { + return ResourceType_Instance; + } + else + { + throw OrthancException(ErrorCode_InexistentItem); + } + } + + + void OrthancIdentifiers::SetLevel(ResourceType level, + const std::string& id) + { + switch (level) + { + case ResourceType_Patient: + SetPatientId(id); + break; + + case ResourceType_Study: + SetStudyId(id); + break; + + case ResourceType_Series: + SetSeriesId(id); + break; + + case ResourceType_Instance: + SetInstanceId(id); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + std::string OrthancIdentifiers::GetLevel(ResourceType level) const + { + switch (level) + { + case ResourceType_Patient: + return GetPatientId(); + + case ResourceType_Study: + return GetStudyId(); + + case ResourceType_Series: + return GetSeriesId(); + + case ResourceType_Instance: + return GetInstanceId(); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/OrthancIdentifiers.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.h Mon Sep 02 17:17:22 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 . + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" + +#include +#include + + +namespace Orthanc +{ + class OrthancIdentifiers : public boost::noncopyable + { + private: + std::unique_ptr patientId_; + std::unique_ptr studyId_; + std::unique_ptr seriesId_; + std::unique_ptr instanceId_; + + public: + OrthancIdentifiers() + { + } + + OrthancIdentifiers(const OrthancIdentifiers& other); + + void SetPatientId(const std::string& id); + + bool HasPatientId() const + { + return patientId_.get() != NULL; + } + + const std::string& GetPatientId() const; + + void SetStudyId(const std::string& id); + + bool HasStudyId() const + { + return studyId_.get() != NULL; + } + + const std::string& GetStudyId() const; + + void SetSeriesId(const std::string& id); + + bool HasSeriesId() const + { + return seriesId_.get() != NULL; + } + + const std::string& GetSeriesId() const; + + void SetInstanceId(const std::string& id); + + bool HasInstanceId() const + { + return instanceId_.get() != NULL; + } + + const std::string& GetInstanceId() const; + + ResourceType DetectLevel() const; + + void SetLevel(ResourceType level, + const std::string& id); + + std::string GetLevel(ResourceType level) const; + }; +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Mon Sep 02 17:17:22 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" diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Mon Sep 02 17:17:22 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 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 tags; - DicomMap::GetMainDicomTags(tags, level); - - for (std::set::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); @@ -1687,7 +1552,8 @@ DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true); DatabaseConstraints query; - query.AddConstraint(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + bool isIdentical; // unused + query.AddConstraint(c.ConvertToDatabaseConstraint(isIdentical, level, DicomTagType_Identifier)); class Operations : public IReadOnlyOperations @@ -2515,7 +2381,7 @@ catch (boost::bad_lexical_cast&) { LOG(ERROR) << "Cannot read the global sequence " - << boost::lexical_cast(sequence_) << ", resetting it"; + << boost::lexical_cast(sequence_) << ", resetting it"; oldValue = 0; } @@ -2814,8 +2680,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 +2806,7 @@ } bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount, - const std::string& patientId) + const std::string& patientId) { if (maximumPatientCount != 0) { @@ -3042,7 +2908,7 @@ }; if (maximumStorageMode == MaxStorageMode_Recycle - && (maximumStorageSize != 0 || maximumPatientCount != 0)) + && (maximumStorageSize != 0 || maximumPatientCount != 0)) { Operations operations(maximumStorageSize, maximumPatientCount); Apply(operations); @@ -3106,9 +2972,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 +3167,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 +3196,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 +3672,76 @@ boost::shared_lock lock(mutex_); return db_.GetDatabaseCapabilities().HasLabelsSupport(); } + + + void StatelessDatabaseOperations::ExecuteFind(FindResponse& response, + const FindRequest& request) + { + class IntegratedFind : public ReadOnlyOperationsT3 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + class FindStage : public ReadOnlyOperationsT3&, const IDatabaseWrapper::Capabilities&, const FindRequest& > + { + 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 ReadOnlyOperationsT4 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteExpand(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); + } + }; + + 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 identifiers; + + FindStage find; + find.Apply(*this, identifiers, capabilities, request); + + ExpandStage expand; + + for (std::list::const_iterator it = identifiers.begin(); it != identifiers.end(); ++it) + { + /** + * Not that the resource might have been deleted (as we are in + * another transaction). The database engine must ignore such + * error cases. + **/ + expand.Apply(*this, response, capabilities, request, *it); + } + } + } } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Database/StatelessDatabaseOperations.h --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Mon Sep 02 17:17:22 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 #include @@ -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,28 @@ { transaction_.ListAllLabels(target); } + + void ExecuteFind(FindResponse& response, + const FindRequest& request, + const IDatabaseWrapper::Capabilities& capabilities) + { + transaction_.ExecuteFind(response, request, capabilities); + } + + void ExecuteFind(std::list& identifiers, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request) + { + transaction_.ExecuteFind(identifiers, capabilities, request); + } + + void ExecuteExpand(FindResponse& response, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier) + { + transaction_.ExecuteExpand(response, capabilities, request, identifier); + } }; @@ -545,7 +579,6 @@ private: - class MainDicomTagsRegistry; class Transaction; IDatabaseWrapper& db_; @@ -798,5 +831,8 @@ const std::set& labels); bool HasLabelsSupport(); + + void ExecuteFind(FindResponse& response, + const FindRequest& request); }; } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/OrthancFindRequestHandler.cpp --- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -30,6 +30,7 @@ #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h" #include "../../OrthancFramework/Sources/MetricsRegistry.h" #include "OrthancConfiguration.h" +#include "ResourceFinder.cpp" #include "Search/DatabaseLookup.h" #include "ServerContext.h" #include "ServerToolbox.h" @@ -39,6 +40,48 @@ namespace Orthanc { + static void CopySequence(ParsedDicomFile& dicom, + const DicomTag& tag, + const Json::Value& source, + const std::string& defaultPrivateCreator, + const std::map& privateCreators) + { + if (source.type() == Json::objectValue && + source.isMember("Type") && + source.isMember("Value") && + source["Type"].asString() == "Sequence" && + source["Value"].type() == Json::arrayValue) + { + Json::Value content = Json::arrayValue; + + for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++) + { + Json::Value item; + Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short); + content.append(item); + } + + if (tag.IsPrivate()) + { + std::map::const_iterator found = privateCreators.find(tag.GetGroup()); + + if (found != privateCreators.end()) + { + dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str()); + } + else + { + dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator); + } + } + else + { + dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */); + } + } + } + + static void AddAnswer(DicomFindAnswers& answers, ServerContext& context, const std::string& publicId, @@ -126,39 +169,7 @@ assert(dicomAsJson != NULL); const Json::Value& source = (*dicomAsJson) [tag->Format()]; - if (source.type() == Json::objectValue && - source.isMember("Type") && - source.isMember("Value") && - source["Type"].asString() == "Sequence" && - source["Value"].type() == Json::arrayValue) - { - Json::Value content = Json::arrayValue; - - for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++) - { - Json::Value item; - Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short); - content.append(item); - } - - if (tag->IsPrivate()) - { - std::map::const_iterator found = privateCreators.find(tag->GetGroup()); - - if (found != privateCreators.end()) - { - dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str()); - } - else - { - dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator); - } - } - else - { - dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */); - } - } + CopySequence(dicom, *tag, source, defaultPrivateCreator, privateCreators); } answers.Add(dicom); @@ -319,6 +330,124 @@ }; + namespace + { + class LookupVisitorV2 : public ResourceFinder::IVisitor + { + private: + DicomFindAnswers& answers_; + DicomArray queryAsArray_; + const std::list& sequencesToReturn_; + std::string defaultPrivateCreator_; // the private creator to use if the group is not defined in the query itself + const std::map& privateCreators_; // the private creators defined in the query itself + std::string retrieveAet_; + + public: + LookupVisitorV2(DicomFindAnswers& answers, + const DicomMap& query, + const std::list& sequencesToReturn, + const std::map& privateCreators) : + answers_(answers), + queryAsArray_(query), + sequencesToReturn_(sequencesToReturn), + privateCreators_(privateCreators) + { + answers_.SetComplete(false); + + { + OrthancConfiguration::ReaderLock lock; + defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator(); + retrieveAet_ = lock.GetConfiguration().GetOrthancAET(); + } + } + + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE + { + DicomMap resourceTags; + resource.GetAllMainDicomTags(resourceTags); + resourceTags.Merge(requestedTags); + + DicomMap result; + + /** + * Add the mandatory "Retrieve AE Title (0008,0054)" tag, which was missing in Orthanc <= 1.7.2. + * http://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_C.4.1.1.3.2 + * https://groups.google.com/g/orthanc-users/c/-7zNTKR_PMU/m/kfjwzEVNAgAJ + **/ + result.SetValue(DICOM_TAG_RETRIEVE_AE_TITLE, retrieveAet_, false /* not binary */); + + for (size_t i = 0; i < queryAsArray_.GetSize(); i++) + { + const DicomTag tag = queryAsArray_.GetElement(i).GetTag(); + + if (tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL) + { + // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052)) + result.SetValue(tag, queryAsArray_.GetElement(i).GetValue()); + } + else if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + // Do not include the encoding, this is handled by class ParsedDicomFile + } + else + { + const DicomValue* value = resourceTags.TestAndGetValue(tag); + + if (value == NULL || + value->IsNull() || + value->IsBinary()) + { + result.SetValue(tag, "", false); + } + else + { + result.SetValue(tag, value->GetContent(), false); + } + } + } + + if (result.GetSize() == 0 && + sequencesToReturn_.empty()) + { + CLOG(WARNING, DICOM) << "The C-FIND request does not return any DICOM tag"; + } + else if (sequencesToReturn_.empty()) + { + answers_.Add(result); + } + else + { + ParsedDicomFile dicom(result, GetDefaultDicomEncoding(), + true /* be permissive, cf. issue #136 */, defaultPrivateCreator_, privateCreators_); + + for (std::list::const_iterator tag = sequencesToReturn_.begin(); + tag != sequencesToReturn_.end(); ++tag) + { + const DicomValue* value = resourceTags.TestAndGetValue(*tag); + if (value != NULL && + value->IsSequence()) + { + CopySequence(dicom, *tag, value->GetSequenceContent(), defaultPrivateCreator_, privateCreators_); + } + else + { + dicom.Replace(*tag, std::string(""), false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator_); + } + } + + answers_.Add(dicom); + } + } + + virtual void MarkAsComplete() ORTHANC_OVERRIDE + { + answers_.SetComplete(true); + } + }; + } + + void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, const DicomMap& input, const std::list& sequencesToReturn, @@ -396,7 +525,6 @@ throw OrthancException(ErrorCode_NotImplemented); } - DicomArray query(*filteredInput); CLOG(INFO, DICOM) << "DICOM C-Find request at level: " << EnumerationToString(level); @@ -410,9 +538,12 @@ } } + std::set requestedTags; + for (std::list::const_iterator it = sequencesToReturn.begin(); it != sequencesToReturn.end(); ++it) { + requestedTags.insert(*it); CLOG(INFO, DICOM) << " (" << it->Format() << ") " << FromDcmtkBridge::GetTagName(*it, "") << " : sequence tag whose content will be copied"; @@ -441,18 +572,26 @@ const DicomTag tag = element.GetTag(); // remove tags that are not used for matching - if (element.GetValue().IsNull() || - tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL || + if (tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL || tag == DICOM_TAG_SPECIFIC_CHARACTER_SET || tag == DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC) // time zone is not directly used for matching. Once we support "Timezone query adjustment", we may use it to adjust date-time filters but for now, just ignore it { continue; } + requestedTags.insert(tag); + + if (element.GetValue().IsNull()) + { + // There is no constraint on this tag + continue; + } + std::string value = element.GetValue().GetContent(); if (value.size() == 0) { // An empty string corresponds to an universal constraint, so we ignore it + requestedTags.insert(tag); continue; } @@ -486,8 +625,28 @@ size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_; - LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators, context_.GetFindStorageAccessMode()); - context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit); + if (true) + { + /** + * EXPERIMENTAL VERSION + **/ + + ResourceFinder finder(level, false /* don't expand */); + finder.SetDatabaseLookup(lookup); + finder.AddRequestedTags(requestedTags); + + LookupVisitorV2 visitor(answers, *filteredInput, sequencesToReturn, privateCreators); + finder.Execute(visitor, context_); + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + + LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators, context_.GetFindStorageAccessMode()); + context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit); + } } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -22,6 +22,8 @@ #include "../PrecompiledHeadersServer.h" +#include "../ResourceFinder.h" + #include "OrthancRestApi.h" #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h" @@ -127,9 +129,24 @@ } + static bool ExpandResource(Json::Value& target, + ServerContext& context, + ResourceType level, + const std::string& identifier, + DicomToJsonFormat format, + bool retrieveMetadata) + { + ResourceFinder finder(level, true /* expand */); + finder.SetOrthancId(level, identifier); + finder.SetRetrieveMetadata(retrieveMetadata); + + return finder.ExecuteOneResource(target, context, format, retrieveMetadata); + } + + // List all the patients, studies, series or instances ---------------------- - static void AnswerListOfResources(RestApiOutput& output, + static void AnswerListOfResources1(RestApiOutput& output, ServerContext& context, const std::list& resources, const std::map& instancesIds, // optional: the id of an instance for each found resource. @@ -183,7 +200,7 @@ } - static void AnswerListOfResources(RestApiOutput& output, + static void AnswerListOfResources2(RestApiOutput& output, ServerContext& context, const std::list& resources, ResourceType level, @@ -196,7 +213,7 @@ std::map > unusedResourcesMainDicomTags; std::map > unusedResourcesDicomAsJson; - AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess); + AnswerListOfResources1(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess); } @@ -226,41 +243,92 @@ ServerIndex& index = OrthancRestApi::GetIndex(call); ServerContext& context = OrthancRestApi::GetContext(call); - std::list result; - - std::set requestedTags; - OrthancRestApi::GetRequestedTags(requestedTags, call); - - if (call.HasArgument("limit") || - call.HasArgument("since")) + if (true) { - if (!call.HasArgument("limit")) + /** + * EXPERIMENTAL VERSION + **/ + + // TODO-FIND: include the FindRequest options parsing in a method (parse from get-arguments and from post payload) + // TODO-FIND: support other values for expand like expand=MainDicomTags,Labels,Parent,SeriesStatus + const bool expand = (call.HasArgument("expand") && + call.GetBooleanArgument("expand", true)); + + std::set requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + + ResourceFinder finder(resourceType, expand); + finder.AddRequestedTags(requestedTags); + + if (call.HasArgument("limit") || + call.HasArgument("since")) { - throw OrthancException(ErrorCode_BadRequest, - "Missing \"limit\" argument for GET request against: " + - call.FlattenUri()); + if (!call.HasArgument("limit")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"limit\" argument for GET request against: " + + call.FlattenUri()); + } + + if (!call.HasArgument("since")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"since\" argument for GET request against: " + + call.FlattenUri()); + } + + uint64_t since = boost::lexical_cast(call.GetArgument("since", "")); + uint64_t limit = boost::lexical_cast(call.GetArgument("limit", "")); + finder.SetLimitsSince(since); + finder.SetLimitsCount(limit); } - if (!call.HasArgument("since")) - { - throw OrthancException(ErrorCode_BadRequest, - "Missing \"since\" argument for GET request against: " + - call.FlattenUri()); - } - - size_t since = boost::lexical_cast(call.GetArgument("since", "")); - size_t limit = boost::lexical_cast(call.GetArgument("limit", "")); - index.GetAllUuids(result, resourceType, since, limit); + Json::Value answer; + finder.Execute(answer, context, OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); } else { - index.GetAllUuids(result, resourceType); + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + + std::list result; + + std::set requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + + if (call.HasArgument("limit") || + call.HasArgument("since")) + { + if (!call.HasArgument("limit")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"limit\" argument for GET request against: " + + call.FlattenUri()); + } + + if (!call.HasArgument("since")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"since\" argument for GET request against: " + + call.FlattenUri()); + } + + size_t since = boost::lexical_cast(call.GetArgument("since", "")); + size_t limit = boost::lexical_cast(call.GetArgument("limit", "")); + index.GetAllUuids(result, resourceType, since, limit); + } + else + { + index.GetAllUuids(result, resourceType); + } + + AnswerListOfResources2(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true), + OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), + requestedTags, + true /* allowStorageAccess */); } - - AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true), - OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), - requestedTags, - true /* allowStorageAccess */); } @@ -289,11 +357,34 @@ std::set 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.SetOrthancId(resourceType, call.GetUriComponent("id", "")); + + Json::Value json; + if (finder.ExecuteOneResource(json, OrthancRestApi::GetContext(call), format, false /* no "Metadata" field */)) + { + 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 +3231,7 @@ bool expand, const std::set& requestedTags) const { - AnswerListOfResources(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); + AnswerListOfResources1(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); } }; } @@ -3252,8 +3343,140 @@ 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)); + + const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human); + + if (request.isMember(KEY_LIMIT)) + { + int64_t tmp = request[KEY_LIMIT].asInt64(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer"); + } + else if (tmp != 0) // This is for compatibility with Orthanc 1.12.4 + { + finder.SetLimitsCount(static_cast(tmp)); + } + } + + if (request.isMember(KEY_SINCE)) + { + int64_t tmp = request[KEY_SINCE].asInt64(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer"); + } + else + { + finder.SetLimitsSince(static_cast(tmp)); + } + } + + { + 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 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, format, false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); + } else { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ bool expand = false; if (request.isMember(KEY_EXPAND)) { @@ -3398,34 +3621,56 @@ 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 requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); - std::list a, b, c; - a.push_back(call.GetUriComponent("id", "")); - - ResourceType type = start; - while (type != end) + if (true) { - b.clear(); - - for (std::list::const_iterator - it = a.begin(); it != a.end(); ++it) + /** + * EXPERIMENTAL VERSION + **/ + + ResourceFinder finder(end, expand); + finder.SetOrthancId(start, call.GetUriComponent("id", "")); + finder.AddRequestedTags(requestedTags); + + Json::Value answer; + finder.Execute(answer, context, format, false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + std::list 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::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 */); } @@ -3538,9 +3783,26 @@ const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); Json::Value resource; - if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags, true /* allowStorageAccess */)) + + if (true) { - call.GetOutput().AnswerJson(resource); + /** + * EXPERIMENTAL VERSION + **/ + if (ExpandResource(resource, OrthancRestApi::GetContext(call), currentType, current, format, false)) + { + call.GetOutput().AnswerJson(resource); + } + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags, true /* allowStorageAccess */)) + { + call.GetOutput().AnswerJson(resource); + } } } @@ -3971,17 +4233,34 @@ for (std::set::const_iterator it = interest.begin(); it != interest.end(); ++it) { - Json::Value item; - std::set emptyRequestedTags; // not supported for bulk content - - if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */)) + if (true) + { + /** + * EXPERIMENTAL VERSION + **/ + Json::Value item; + if (ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata)) + { + answer.append(item); + } + } + else { - if (metadata) + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + Json::Value item; + std::set emptyRequestedTags; // not supported for bulk content + + if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */)) { - AddMetadata(item[METADATA], index, *it, level); + if (metadata) + { + AddMetadata(item[METADATA], index, *it, level); + } + + answer.append(item); } - - answer.append(item); } } } @@ -3998,19 +4277,36 @@ Json::Value item; std::set emptyRequestedTags; // not supported for bulk content - if (index.LookupResourceType(level, *it) && - OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */)) + if (true) { - if (metadata) + /** + * EXPERIMENTAL VERSION + **/ + if (index.LookupResourceType(level, *it) && + ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata)) { - AddMetadata(item[METADATA], index, *it, level); + answer.append(item); } - - answer.append(item); } else { - CLOG(INFO, HTTP) << "Unknown resource during a bulk content retrieval: " << *it; + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + if (index.LookupResourceType(level, *it) && + OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */)) + { + if (metadata) + { + AddMetadata(item[METADATA], index, *it, level); + } + + answer.append(item); + } + else + { + CLOG(INFO, HTTP) << "Unknown resource during a bulk content retrieval: " << *it; + } } } } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/OrthancWebDav.cpp --- a/OrthancServer/Sources/OrthancWebDav.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -28,6 +28,7 @@ #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h" #include "../../OrthancFramework/Sources/Logging.h" +#include "ResourceFinder.h" #include "Search/DatabaseLookup.h" #include "ServerContext.h" @@ -50,6 +51,20 @@ { return boost::posix_time::second_clock::universal_time(); } + + + static void ParseTime(boost::posix_time::ptime& target, + const std::string& value) + { + try + { + target = boost::posix_time::from_iso_string(value); + } + catch (std::exception& e) + { + target = GetNow(); + } + } static void LookupTime(boost::posix_time::ptime& target, @@ -62,17 +77,12 @@ int64_t revision; // Ignored if (context.GetIndex().LookupMetadata(value, revision, publicId, level, metadata)) { - try - { - target = boost::posix_time::from_iso_string(value); - return; - } - catch (std::exception& e) - { - } + ParseTime(target, value); } - - target = GetNow(); + else + { + target = GetNow(); + } } @@ -169,6 +179,98 @@ }; + class OrthancWebDav::DicomIdentifiersVisitorV2 : public ResourceFinder::IVisitor + { + private: + bool isComplete_; + Collection& target_; + + public: + explicit DicomIdentifiersVisitorV2(Collection& target) : + isComplete_(false), + target_(target) + { + } + + virtual void MarkAsComplete() ORTHANC_OVERRIDE + { + isComplete_ = true; // TODO + } + + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE + { + DicomMap resourceTags; + resource.GetMainDicomTags(resourceTags, resource.GetLevel()); + + std::string uid; + bool hasUid; + + std::string time; + bool hasTime; + + switch (resource.GetLevel()) + { + case ResourceType_Study: + hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_STUDY_INSTANCE_UID, false); + hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate); + break; + + case ResourceType_Series: + hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SERIES_INSTANCE_UID, false); + hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate); + break; + + case ResourceType_Instance: + hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SOP_INSTANCE_UID, false); + hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_Instance_ReceptionDate); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (hasUid && + !uid.empty()) + { + std::unique_ptr item; + + if (resource.GetLevel() == ResourceType_Instance) + { + FileInfo info; + if (resource.LookupAttachment(info, FileContentType_Dicom)) + { + std::unique_ptr f(new File(uid + ".dcm")); + f->SetMimeType(MimeType_Dicom); + f->SetContentLength(info.GetUncompressedSize()); + item.reset(f.release()); + } + } + else + { + item.reset(new Folder(uid)); + } + + if (item.get() != NULL) + { + if (hasTime) + { + boost::posix_time::ptime t; + ParseTime(t, time); + item->SetCreationTime(t); + } + else + { + item->SetCreationTime(GetNow()); + } + + target_.AddResource(item.release()); + } + } + } + }; + + class OrthancWebDav::DicomFileVisitor : public ServerContext::ILookupVisitor { private: @@ -221,6 +323,60 @@ }; + class OrthancWebDav::DicomFileVisitorV2 : public ResourceFinder::IVisitor + { + private: + ServerContext& context_; + bool success_; + std::string& target_; + boost::posix_time::ptime& time_; + + public: + DicomFileVisitorV2(ServerContext& context, + std::string& target, + boost::posix_time::ptime& time) : + context_(context), + success_(false), + target_(target), + time_(time) + { + } + + bool IsSuccess() const + { + return success_; + } + + virtual void MarkAsComplete() ORTHANC_OVERRIDE + { + } + + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE + { + if (success_) + { + success_ = false; // Two matches => Error + } + else + { + std::string s; + if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_ReceptionDate)) + { + ParseTime(time_, s); + } + else + { + time_ = GetNow(); + } + + context_.ReadDicom(target_, resource.GetIdentifier()); + success_ = true; + } + } + }; + + class OrthancWebDav::OrthancJsonVisitor : public ServerContext::ILookupVisitor { private: @@ -955,7 +1111,7 @@ std::string year_; std::string month_; - class Visitor : public ServerContext::ILookupVisitor + class Visitor : public ResourceFinder::IVisitor { private: std::list& resources_; @@ -966,21 +1122,14 @@ { } - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - virtual void MarkAsComplete() ORTHANC_OVERRIDE { } - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE { - resources_.push_back(publicId); + resources_.push_back(resource.GetIdentifier()); } }; @@ -992,7 +1141,10 @@ true /* case sensitive */, true /* mandatory tag */); Visitor visitor(resources); - GetContext().Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); + + ResourceFinder finder(ResourceType_Study, false /* no expand */); + finder.SetDatabaseLookup(query); + finder.Execute(visitor, GetContext()); } virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE @@ -1025,7 +1177,7 @@ std::string year_; const Templates& templates_; - class Visitor : public ServerContext::ILookupVisitor + class Visitor : public ResourceFinder::IVisitor { private: std::set months_; @@ -1036,20 +1188,16 @@ return months_; } - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - virtual void MarkAsComplete() ORTHANC_OVERRIDE { } - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE { + DicomMap mainDicomTags; + resource.GetMainDicomTags(mainDicomTags, ResourceType_Study); + std::string s; if (mainDicomTags.LookupStringValue(s, DICOM_TAG_STUDY_DATE, false) && s.size() == 8) @@ -1071,7 +1219,10 @@ true /* case sensitive */, true /* mandatory tag */); Visitor visitor; - context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); + + ResourceFinder finder(ResourceType_Study, false /* no expand */); + finder.SetDatabaseLookup(query); + finder.Execute(visitor, context_); for (std::set::const_iterator it = visitor.GetMonths().begin(); it != visitor.GetMonths().end(); ++it) @@ -1165,7 +1316,7 @@ }; - class OrthancWebDav::DicomDeleteVisitor : public ServerContext::ILookupVisitor + class OrthancWebDav::DicomDeleteVisitor : public ResourceFinder::IVisitor { private: ServerContext& context_; @@ -1179,22 +1330,15 @@ { } - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - virtual void MarkAsComplete() ORTHANC_OVERRIDE { } - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags /* unused */, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE { Json::Value info; - context_.DeleteResource(info, publicId, level_); + context_.DeleteResource(info, resource.GetIdentifier(), level_); } }; @@ -1455,9 +1599,48 @@ return false; } - DicomIdentifiersVisitor visitor(context_, collection, level); - context_.Apply(visitor, query, level, 0 /* since */, limit); - + if (true) + { + /** + * EXPERIMENTAL VERSION + **/ + + ResourceFinder finder(level, false /* don't expand */); + finder.SetDatabaseLookup(query); + finder.SetRetrieveMetadata(true); + + switch (level) + { + case ResourceType_Study: + finder.AddRequestedTag(DICOM_TAG_STUDY_INSTANCE_UID); + break; + + case ResourceType_Series: + finder.AddRequestedTag(DICOM_TAG_SERIES_INSTANCE_UID); + break; + + case ResourceType_Instance: + finder.AddRequestedTag(DICOM_TAG_SOP_INSTANCE_UID); + finder.SetRetrieveAttachments(true); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + DicomIdentifiersVisitorV2 visitor(collection); + finder.Execute(visitor, context_); + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + + DicomIdentifiersVisitor visitor(context_, collection, level); + context_.Apply(visitor, query, level, 0 /* since */, limit); + } + return true; } else if (path[0] == BY_PATIENTS || @@ -1478,6 +1661,33 @@ } + static bool GetOrthancJson(std::string& target, + ServerContext& context, + ResourceType level, + const DatabaseLookup& query) + { + ResourceFinder finder(level, true /* expand */); + finder.SetDatabaseLookup(query); + + Json::Value expanded; + finder.Execute(expanded, context, DicomToJsonFormat_Human, false /* don't add "Metadata" */); + + if (expanded.size() != 1) + { + return false; + } + else + { + target = expanded[0].toStyledString(); + + // Replace UNIX newlines with DOS newlines + boost::replace_all(target, "\n", "\r\n"); + + return true; + } + } + + bool OrthancWebDav::GetFileContent(MimeType& mime, std::string& content, boost::posix_time::ptime& modificationTime, @@ -1495,12 +1705,25 @@ DatabaseLookup query; query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1], true /* case sensitive */, true /* mandatory tag */); - - OrthancJsonVisitor visitor(context_, content, ResourceType_Study); - context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); mime = MimeType_Json; - return visitor.IsSuccess(); + + if (true) + { + /** + * EXPERIMENTAL VERSION + **/ + return GetOrthancJson(content, context_, ResourceType_Study, query); + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + OrthancJsonVisitor visitor(context_, content, ResourceType_Study); + context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); + return visitor.IsSuccess(); + } } else if (path.size() == 4 && path[3] == SERIES_INFO) @@ -1511,11 +1734,24 @@ query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2], true /* case sensitive */, true /* mandatory tag */); - OrthancJsonVisitor visitor(context_, content, ResourceType_Series); - context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */); + mime = MimeType_Json; - mime = MimeType_Json; - return visitor.IsSuccess(); + if (true) + { + /** + * EXPERIMENTAL VERSION + **/ + return GetOrthancJson(content, context_, ResourceType_Series, query); + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + OrthancJsonVisitor visitor(context_, content, ResourceType_Series); + context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */); + return visitor.IsSuccess(); + } } else if (path.size() == 4 && boost::ends_with(path[3], ".dcm")) @@ -1530,11 +1766,32 @@ query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid, true /* case sensitive */, true /* mandatory tag */); - DicomFileVisitor visitor(context_, content, modificationTime); - context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */); - mime = MimeType_Dicom; - return visitor.IsSuccess(); + + if (true) + { + /** + * EXPERIMENTAL VERSION + **/ + ResourceFinder finder(ResourceType_Instance, false /* no expand */); + finder.SetDatabaseLookup(query); + finder.SetRetrieveMetadata(true); + finder.SetRetrieveAttachments(true); + + DicomFileVisitorV2 visitor(context_, content, modificationTime); + finder.Execute(visitor, context_); + + return visitor.IsSuccess(); + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + DicomFileVisitor visitor(context_, content, modificationTime); + context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */); + return visitor.IsSuccess(); + } } else { @@ -1655,7 +1912,10 @@ } DicomDeleteVisitor visitor(context_, level); - context_.Apply(visitor, query, level, 0 /* since */, 0 /* no limit */); + + ResourceFinder finder(level, false /* no expand */); + finder.SetDatabaseLookup(query); + finder.Execute(visitor, context_); return true; } else diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/OrthancWebDav.h --- a/OrthancServer/Sources/OrthancWebDav.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/OrthancWebDav.h Mon Sep 02 17:17:22 2024 +0200 @@ -39,7 +39,9 @@ class DicomDeleteVisitor; class DicomFileVisitor; + class DicomFileVisitorV2; class DicomIdentifiersVisitor; + class DicomIdentifiersVisitorV2; class InstancesOfSeries; class InternalNode; class ListOfResources; @@ -47,6 +49,7 @@ class ListOfStudiesByMonth; class ListOfStudiesByYear; class OrthancJsonVisitor; + class OrthancJsonVisitorV2; class ResourcesIndex; class RootNode; class SingleDicomResource; diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/ResourceFinder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ResourceFinder.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -0,0 +1,1095 @@ +/** + * 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 . + **/ + + +#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& children = resource.GetChildrenIdentifiers(level); + requestedTags.SetValue(tag, boost::lexical_cast(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 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 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 values; + resource.GetChildrenMetadataValues(values, ResourceType_Instance, MetadataType_Instance_IndexInSeries); + + std::set instances; + + for (std::set::const_iterator + it = values.begin(); it != values.end(); ++it) + { + int64_t index; + + if (!SerializationToolbox::ParseInteger64(index, *it)) + { + return SeriesStatus_Unknown; + } + + if (index <= 0 || + index > static_cast(expectedNumberOfInstances)) + { + // Out-of-range instance index + return SeriesStatus_Inconsistent; + } + + if (instances.find(index) != instances.end()) + { + // Twice the same instance index + return SeriesStatus_Inconsistent; + } + + instances.insert(index); + } + + if (instances.size() == static_cast(expectedNumberOfInstances)) + { + return SeriesStatus_Complete; + } + else + { + return SeriesStatus_Missing; + } + } + + + void ResourceFinder::Expand(Json::Value& target, + const FindResponse::Resource& resource, + ServerIndex& index, + DicomToJsonFormat format, + bool includeAllMetadata) const + { + /** + * This method closely follows "SerializeExpandedResource()" in + * "ServerContext.cpp" from Orthanc 1.12.4. + **/ + + if (!expand_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (resource.GetLevel() != request_.GetLevel()) + { + throw OrthancException(ErrorCode_InternalError); + } + + target = Json::objectValue; + + target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true); + target["ID"] = resource.GetIdentifier(); + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + break; + + case ResourceType_Study: + target["ParentPatient"] = resource.GetParentIdentifier(); + break; + + case ResourceType_Series: + target["ParentStudy"] = resource.GetParentIdentifier(); + break; + + case ResourceType_Instance: + target["ParentSeries"] = resource.GetParentIdentifier(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (resource.GetLevel() != ResourceType_Instance) + { + const std::set& children = resource.GetChildrenIdentifiers(GetChildResourceType(resource.GetLevel())); + + Json::Value c = Json::arrayValue; + for (std::set::const_iterator + it = children.begin(); it != children.end(); ++it) + { + c.append(*it); + } + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + target["Studies"] = c; + break; + + case ResourceType_Study: + target["Series"] = c; + break; + + case ResourceType_Series: + target["Instances"] = c; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + case ResourceType_Study: + break; + + case ResourceType_Series: + { + uint32_t expectedNumberOfInstances; + SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource); + + target["Status"] = EnumerationToString(status); + + 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(info.GetUncompressedSize()); + target["FileUuid"] = info.GetUuid(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + + static const char* const INDEX_IN_SERIES = "IndexInSeries"; + + std::string s; + uint32_t indexInSeries; + if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) && + SerializationToolbox::ParseUnsignedInteger32(indexInSeries, s)) + { + target[INDEX_IN_SERIES] = indexInSeries; + } + 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::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& m = resource.GetMetadata(resource.GetLevel()); + + Json::Value metadata = Json::objectValue; + + for (std::map::const_iterator it = m.begin(); it != m.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second; + } + + target["Metadata"] = metadata; + } + } + + + void ResourceFinder::UpdateRequestLimits() + { + // By default, use manual paging + pagingMode_ = PagingMode_FullManual; + + if (databaseLimits_ != 0) + { + request_.SetLimits(0, databaseLimits_ + 1); + } + else + { + request_.ClearLimits(); + } + + if (lookup_.get() == NULL && + (hasLimitsSince_ || hasLimitsCount_)) + { + pagingMode_ = PagingMode_FullDatabase; + request_.SetLimits(limitsSince_, limitsCount_); + } + + if (lookup_.get() != NULL && + isSimpleLookup_ && + (hasLimitsSince_ || hasLimitsCount_)) + { + /** + * TODO-FIND: "IDatabaseWrapper::ApplyLookupResources()" only + * accept the "limit" argument. The "since" must be implemented + * manually. + **/ + + if (hasLimitsSince_ && + limitsSince_ != 0) + { + pagingMode_ = PagingMode_ManualSkip; + request_.SetLimits(0, limitsCount_ + limitsSince_); + } + else + { + pagingMode_ = PagingMode_FullDatabase; + request_.SetLimits(0, limitsCount_); + } + } + + // TODO-FIND: More cases could be added, depending on "GetDatabaseCapabilities()" + } + + + ResourceFinder::ResourceFinder(ResourceType level, + bool expand) : + request_(level), + databaseLimits_(0), + isSimpleLookup_(true), + pagingMode_(PagingMode_FullManual), + hasLimitsSince_(false), + hasLimitsCount_(false), + limitsSince_(0), + limitsCount_(0), + expand_(expand), + allowStorageAccess_(true), + hasRequestedTags_(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::SetLimitsSince(uint64_t since) + { + if (hasLimitsSince_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + hasLimitsSince_ = true; + limitsSince_ = since; + UpdateRequestLimits(); + } + } + + + void ResourceFinder::SetLimitsCount(uint64_t count) + { + if (hasLimitsCount_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + hasLimitsCount_ = true; + limitsCount_ = count; + UpdateRequestLimits(); + } + } + + + void ResourceFinder::SetDatabaseLookup(const DatabaseLookup& lookup) + { + MainDicomTagsRegistry registry; + + lookup_.reset(lookup.Clone()); + + for (size_t i = 0; i < lookup.GetConstraintsCount(); i++) + { + DicomTag tag = lookup.GetConstraint(i).GetTag(); + if (IsComputedTag(tag)) + { + AddRequestedTag(tag); + } + else + { + ResourceType level; + DicomTagType tagType; + registry.LookupTag(level, tagType, tag); + if (tagType == DicomTagType_Generic) + { + AddRequestedTag(tag); + } + } + } + + isSimpleLookup_ = 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); + } + } + + UpdateRequestLimits(); + } + + + void ResourceFinder::AddRequestedTag(const DicomTag& tag) + { + if (DicomMap::IsMainDicomTag(tag, ResourceType_Patient)) + { + if (request_.GetLevel() == ResourceType_Patient) + { + request_.SetRetrieveMainDicomTags(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()". + **/ + requestedStudyTags_.insert(tag); + + if (request_.GetLevel() == ResourceType_Study) + { + request_.SetRetrieveMainDicomTags(true); + } + else + { + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(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 + { + if (request_.GetLevel() == ResourceType_Study) + { + request_.SetRetrieveMainDicomTags(true); + } + else + { + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(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 + { + if (request_.GetLevel() == ResourceType_Series) + { + request_.SetRetrieveMainDicomTags(true); + } + else + { + request_.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(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 + { + request_.SetRetrieveMainDicomTags(true); + 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& tags) + { + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + AddRequestedTag(*it); + } + } + + + static void InjectRequestedTags(DicomMap& requestedTags, + std::set& missingTags /* out */, + const FindResponse::Resource& resource, + ResourceType level, + const std::set& tags) + { + if (!tags.empty()) + { + DicomMap m; + resource.GetMainDicomTags(m, level); + + for (std::set::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& 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::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(IVisitor& visitor, + ServerContext& context) const + { + FindResponse response; + context.GetIndex().ExecuteFind(response, request_); + + bool complete; + + switch (pagingMode_) + { + case PagingMode_FullDatabase: + case PagingMode_ManualSkip: + complete = true; + break; + + case PagingMode_FullManual: + complete = (databaseLimits_ == 0 || + response.GetSize() <= databaseLimits_); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + 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 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 (pagingMode_ == PagingMode_FullDatabase) + { + visitor.Apply(resource, requestedTags); + } + else + { + if (hasLimitsSince_ && + skipped < limitsSince_) + { + skipped++; + } + else if (hasLimitsCount_ && + countResults >= limitsCount_) + { + // Too many results, don't mark as complete + complete = false; + break; + } + else + { + visitor.Apply(resource, requestedTags); + countResults++; + } + } + } + } + + if (complete) + { + visitor.MarkAsComplete(); + } + } + + + void ResourceFinder::Execute(Json::Value& target, + ServerContext& context, + DicomToJsonFormat format, + bool includeAllMetadata) const + { + class Visitor : public IVisitor + { + private: + const ResourceFinder& that_; + ServerIndex& index_; + Json::Value& target_; + DicomToJsonFormat format_; + bool hasRequestedTags_; + bool includeAllMetadata_; + + public: + Visitor(const ResourceFinder& that, + ServerIndex& index, + Json::Value& target, + DicomToJsonFormat format, + bool hasRequestedTags, + bool includeAllMetadata) : + that_(that), + index_(index), + target_(target), + format_(format), + hasRequestedTags_(hasRequestedTags), + includeAllMetadata_(includeAllMetadata) + { + } + + virtual void Apply(const FindResponse::Resource& resource, + const DicomMap& requestedTags) ORTHANC_OVERRIDE + { + if (that_.expand_) + { + Json::Value item; + that_.Expand(item, resource, index_, format_, includeAllMetadata_); + + if (hasRequestedTags_) + { + static const char* const REQUESTED_TAGS = "RequestedTags"; + item[REQUESTED_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(item[REQUESTED_TAGS], requestedTags, format_); + } + + target_.append(item); + } + else + { + target_.append(resource.GetIdentifier()); + } + } + + virtual void MarkAsComplete() ORTHANC_OVERRIDE + { + } + }; + + target = Json::arrayValue; + + Visitor visitor(*this, context.GetIndex(), target, format, hasRequestedTags_, includeAllMetadata); + Execute(visitor, context); + } + + + bool ResourceFinder::ExecuteOneResource(Json::Value& target, + ServerContext& context, + DicomToJsonFormat format, + bool includeAllMetadata) const + { + Json::Value answer; + Execute(answer, context, format, includeAllMetadata); + + 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; + } + } +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/ResourceFinder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ResourceFinder.h Mon Sep 02 17:17:22 2024 +0200 @@ -0,0 +1,182 @@ +/** + * 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 . + **/ + + +#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, + const DicomMap& requestedTags) = 0; + + virtual void MarkAsComplete() = 0; + }; + + private: + enum PagingMode + { + PagingMode_FullDatabase, + PagingMode_FullManual, + PagingMode_ManualSkip + }; + + FindRequest request_; + uint64_t databaseLimits_; + std::unique_ptr lookup_; + bool isSimpleLookup_; + PagingMode pagingMode_; + bool hasLimitsSince_; + bool hasLimitsCount_; + uint64_t limitsSince_; + uint64_t limitsCount_; + bool expand_; + bool allowStorageAccess_; + bool hasRequestedTags_; + std::set requestedPatientTags_; + std::set requestedStudyTags_; + std::set requestedSeriesTags_; + std::set requestedInstanceTags_; + std::set requestedTagsFromFileStorage_; + std::set requestedComputedTags_; + + 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 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 SetLimitsSince(uint64_t since); + + void SetLimitsCount(uint64_t count); + + void SetDatabaseLookup(const DatabaseLookup& lookup); + + void AddRequestedTag(const DicomTag& tag); + + void AddRequestedTags(const std::set& tags); + + void SetLabels(const std::set& 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 SetRetrieveMetadata(bool retrieve) + { + request_.SetRetrieveMetadata(retrieve); + } + + void SetRetrieveAttachments(bool retrieve) + { + request_.SetRetrieveAttachments(retrieve); + } + + // NB: "index" is only used in this method to fill the "IsStable" information + void Expand(Json::Value& target, + const FindResponse::Resource& resource, + ServerIndex& index, + DicomToJsonFormat format, + bool includeAllMetadata /* Same as: ExpandResourceFlags_IncludeAllMetadata */) const; + + void Execute(IVisitor& visitor, + ServerContext& context) const; + + void Execute(Json::Value& target, + ServerContext& context, + DicomToJsonFormat format, + bool includeAllMetadata) const; + + bool ExecuteOneResource(Json::Value& target, + ServerContext& context, + DicomToJsonFormat format, + bool includeAllMetadata) const; + }; +} diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Search/DatabaseConstraint.cpp --- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -37,6 +37,7 @@ # include #endif +#include #include @@ -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(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 j = 0; j < constraint.GetValuesCount(); j++) + { + if (first) + { + first = false; + } + else + { + s += ", "; + } + s += constraint.GetValue(j); + } + s += "]"; + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + s += "\n"; + } + + return s; + } } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Search/DatabaseConstraint.h --- a/OrthancServer/Sources/Search/DatabaseConstraint.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.h Mon Sep 02 17:17:22 2024 +0200 @@ -178,5 +178,7 @@ } const DatabaseConstraint& GetConstraint(size_t index) const; + + std::string Format() const; }; } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Search/DicomTagConstraint.cpp --- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -215,6 +215,24 @@ } + static bool HasIntersection(const std::set& expected, + const std::string& values) + { + std::vector 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 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 references; + for (std::set::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: @@ -342,7 +369,8 @@ } - DatabaseConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level, + DatabaseConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(bool& isIdentical, + ResourceType level, DicomTagType tagType) const { bool isIdentifier, caseSensitive; @@ -365,13 +393,21 @@ std::vector values; values.reserve(values_.size()); - + + isIdentical = true; + for (std::set::const_iterator it = values_.begin(); it != values_.end(); ++it) { if (isIdentifier) { - values.push_back(ServerToolbox::NormalizeIdentifier(*it)); + std::string normalized = ServerToolbox::NormalizeIdentifier(*it); + values.push_back(normalized); + + if (normalized != *it) + { + isIdentical = false; + } } else { diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Search/DicomTagConstraint.h --- a/OrthancServer/Sources/Search/DicomTagConstraint.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.h Mon Sep 02 17:17:22 2024 +0200 @@ -109,7 +109,8 @@ std::string Format() const; - DatabaseConstraint* ConvertToDatabaseConstraint(ResourceType level, + DatabaseConstraint* ConvertToDatabaseConstraint(bool& isIdentical /* out */, + ResourceType level, DicomTagType tagType) const; }; } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/Search/ISqlLookupFormatter.h diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Mon Sep 02 17:17:22 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" @@ -1549,8 +1550,7 @@ size_t since, size_t limit) { - unsigned int databaseLimit = (queryLevel == ResourceType_Instance ? - limitFindInstances_ : limitFindResults_); + const uint64_t databaseLimit = GetDatabaseLimits(queryLevel); std::vector resources, instances; const DicomTagConstraint* dicomModalitiesConstraint = NULL; @@ -1567,10 +1567,8 @@ fastLookup->RemoveConstraint(DICOM_TAG_MODALITIES_IN_STUDY); } - { - const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); - GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, labels, labelsConstraint, lookupLimit); - } + const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); + GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, labels, labelsConstraint, lookupLimit); bool complete = (databaseLimit == 0 || resources.size() <= databaseLimit); @@ -2137,124 +2135,137 @@ static void SerializeExpandedResource(Json::Value& target, const ExpandedResource& resource, DicomToJsonFormat format, - const std::set& requestedTags) + const std::set& requestedTags, + ExpandResourceFlags expandFlags) { target = Json::objectValue; target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true); target["ID"] = resource.GetPublicId(); - switch (resource.GetLevel()) - { - case ResourceType_Patient: - break; - - case ResourceType_Study: - target["ParentPatient"] = resource.parentId_; - break; - - case ResourceType_Series: - target["ParentStudy"] = resource.parentId_; - break; - - case ResourceType_Instance: - target["ParentSeries"] = resource.parentId_; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - switch (resource.GetLevel()) + if (!resource.parentId_.empty()) { - case ResourceType_Patient: - case ResourceType_Study: - case ResourceType_Series: + switch (resource.GetLevel()) { - Json::Value c = Json::arrayValue; - - for (std::list::const_iterator - it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it) - { - c.append(*it); - } - - if (resource.GetLevel() == ResourceType_Patient) - { - target["Studies"] = c; - } - else if (resource.GetLevel() == ResourceType_Study) - { - target["Series"] = c; - } - else - { - target["Instances"] = c; - } - break; + case ResourceType_Patient: + break; + + case ResourceType_Study: + target["ParentPatient"] = resource.parentId_; + break; + + case ResourceType_Series: + target["ParentStudy"] = resource.parentId_; + break; + + case ResourceType_Instance: + target["ParentSeries"] = resource.parentId_; + break; + + default: + throw OrthancException(ErrorCode_InternalError); } - - case ResourceType_Instance: - break; - - default: - throw OrthancException(ErrorCode_InternalError); } - switch (resource.GetLevel()) + if ((expandFlags & ExpandResourceFlags_IncludeChildren) != 0) { - case ResourceType_Patient: - case ResourceType_Study: - break; - - case ResourceType_Series: - if (resource.expectedNumberOfInstances_ < 0) + switch (resource.GetLevel()) + { + case ResourceType_Patient: + case ResourceType_Study: + case ResourceType_Series: { - target["ExpectedNumberOfInstances"] = Json::nullValue; + Json::Value c = Json::arrayValue; + + for (std::list::const_iterator + it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it) + { + c.append(*it); + } + + if (resource.GetLevel() == ResourceType_Patient) + { + target["Studies"] = c; + } + else if (resource.GetLevel() == ResourceType_Study) + { + target["Series"] = c; + } + else + { + target["Instances"] = c; + } + break; } - else - { - target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_; - } - target["Status"] = resource.status_; - break; - - case ResourceType_Instance: + + case ResourceType_Instance: + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + if ((expandFlags & ExpandResourceFlags_IncludeMetadata) != 0) + { + switch (resource.GetLevel()) { - target["FileSize"] = static_cast(resource.fileSize_); - target["FileUuid"] = resource.fileUuid_; - - if (resource.indexInSeries_ < 0) + case ResourceType_Patient: + case ResourceType_Study: + break; + + case ResourceType_Series: + if (resource.expectedNumberOfInstances_ < 0) + { + target["ExpectedNumberOfInstances"] = Json::nullValue; + } + else + { + target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_; + } + target["Status"] = resource.status_; + break; + + case ResourceType_Instance: { - target["IndexInSeries"] = Json::nullValue; + target["FileSize"] = static_cast(resource.fileSize_); + target["FileUuid"] = resource.fileUuid_; + + if (resource.indexInSeries_ < 0) + { + target["IndexInSeries"] = Json::nullValue; + } + else + { + target["IndexInSeries"] = resource.indexInSeries_; + } + + break; } - else - { - target["IndexInSeries"] = resource.indexInSeries_; - } - - break; + + default: + throw OrthancException(ErrorCode_InternalError); } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - if (!resource.anonymizedFrom_.empty()) - { - target["AnonymizedFrom"] = resource.anonymizedFrom_; - } - if (!resource.modifiedFrom_.empty()) - { - target["ModifiedFrom"] = resource.modifiedFrom_; + if (!resource.anonymizedFrom_.empty()) + { + target["AnonymizedFrom"] = resource.anonymizedFrom_; + } + + if (!resource.modifiedFrom_.empty()) + { + target["ModifiedFrom"] = resource.modifiedFrom_; + } } if (resource.GetLevel() == ResourceType_Patient || resource.GetLevel() == ResourceType_Study || resource.GetLevel() == ResourceType_Series) { - target["IsStable"] = resource.isStable_; + if ((expandFlags & ExpandResourceFlags_IncludeIsStable) != 0) + { + target["IsStable"] = resource.isStable_; + } if (!resource.lastUpdate_.empty()) { @@ -2262,38 +2273,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; @@ -2304,6 +2319,19 @@ target["Labels"] = labels; } + + // new in Orthanc 1.12.4 + if ((expandFlags & ExpandResourceFlags_IncludeAllMetadata) != 0) + { + Json::Value metadata = Json::objectValue; + + for (std::map::const_iterator it = resource.metadata_.begin(); it != resource.metadata_.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second; + } + + target["Metadata"] = metadata; + } } @@ -2549,9 +2577,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; } @@ -2704,5 +2732,4 @@ return elapsed.total_seconds(); } - } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.h Mon Sep 02 17:17:22 2024 +0200 @@ -445,6 +445,11 @@ void Stop(); + uint64_t GetDatabaseLimits(ResourceType level) const + { + return (level == ResourceType_Instance ? limitFindInstances_ : limitFindResults_); + } + void Apply(ILookupVisitor& visitor, const DatabaseLookup& lookup, ResourceType queryLevel, diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/ServerIndex.h --- a/OrthancServer/Sources/ServerIndex.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/ServerIndex.h Mon Sep 02 17:17:22 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); }; } diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp --- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -744,10 +744,18 @@ } else { - ExpandedResource originalStudy; - if (GetContext().GetIndex().ExpandResource(originalStudy, *studyId, ResourceType_Study, emptyRequestedTags, ExpandResourceFlags_IncludeMainDicomTags)) + FindRequest request(ResourceType_Study); + request.SetOrthancStudyId(*studyId); + request.SetRetrieveMainDicomTags(true); + + FindResponse response; + GetContext().GetIndex().ExecuteFind(response, request); + + if (response.GetSize() == 1) { - targetPatientId = originalStudy.GetMainDicomTags().GetStringValue(DICOM_TAG_PATIENT_ID, "", false); + DicomMap tags; + response.GetResourceByIndex(0).GetMainDicomTags(tags, ResourceType_Study); + targetPatientId = tags.GetStringValue(DICOM_TAG_PATIENT_ID, "", false); } else { @@ -762,22 +770,34 @@ // if the patient exists, check how many child studies it has. if (lookupPatientResult.size() >= 1) { - ExpandedResource targetPatient; - - if (GetContext().GetIndex().ExpandResource(targetPatient, lookupPatientResult[0], ResourceType_Patient, emptyRequestedTags, static_cast(ExpandResourceFlags_IncludeMainDicomTags | ExpandResourceFlags_IncludeChildren))) + FindRequest request(ResourceType_Patient); + request.SetOrthancPatientId(lookupPatientResult[0]); + request.SetRetrieveMainDicomTags(true); + request.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(true); + + FindResponse response; + GetContext().GetIndex().ExecuteFind(response, request); + + if (response.GetSize() == 1) { - const std::list childrenIds = targetPatient.childrenIds_; + const FindResponse::Resource& targetPatient = response.GetResourceByIndex(0); + + const std::set& childrenIds = targetPatient.GetChildrenIdentifiers(ResourceType_Study); + bool targetPatientHasOtherStudies = childrenIds.size() > 1; if (childrenIds.size() == 1) { - targetPatientHasOtherStudies = std::find(childrenIds.begin(), childrenIds.end(), *studyId) == childrenIds.end(); // if the patient has one study that is not the one being modified + targetPatientHasOtherStudies = (childrenIds.find(*studyId) == childrenIds.end()); // if the patient has one study that is not the one being modified } if (targetPatientHasOtherStudies) { + DicomMap mainDicomTags; + targetPatient.GetMainDicomTags(mainDicomTags, ResourceType_Patient); + // this is allowed if all patient replacedTags do match the target patient tags DicomMap targetPatientTags; - targetPatient.GetMainDicomTags().ExtractPatientInformation(targetPatientTags); + mainDicomTags.ExtractPatientInformation(targetPatientTags); std::set mainPatientTags; DicomMap::GetMainDicomTags(mainPatientTags, ResourceType_Patient); diff -r 8bb3f2fca242 -r 796cb17db15c OrthancServer/UnitTestsSources/ServerIndexTests.cpp --- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Mon Sep 02 17:17:22 2024 +0200 @@ -167,7 +167,8 @@ DicomTagConstraint c(tag, type, value, true, true); DatabaseConstraints lookup; - lookup.AddConstraint(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + bool isEquivalent; // unused + lookup.AddConstraint(c.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier)); std::set noLabel; transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */); @@ -187,8 +188,9 @@ DicomTagConstraint c2(tag, type2, value2, true, true); DatabaseConstraints lookup; - lookup.AddConstraint(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); - lookup.AddConstraint(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + bool isEquivalent; // unused + lookup.AddConstraint(c1.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier)); + lookup.AddConstraint(c2.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier)); std::set noLabel; transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);