Mercurial > hg > orthanc
changeset 5744:9d6167ddcb35 find-refactoring-clean
merged default -> find-refactoring-clean
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Thu, 29 Aug 2024 15:41:42 +0200 |
parents | e63538a6d9de (diff) 8bb3f2fca242 (current diff) |
children | bbeefd4567dc |
files | NEWS OrthancServer/Sources/Database/FindResponse.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h |
diffstat | 44 files changed, 5682 insertions(+), 2156 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Thu Aug 29 13:46:49 2024 +0200 +++ b/NEWS Thu Aug 29 15:41:42 2024 +0200 @@ -1,13 +1,15 @@ Pending changes in the mainline =============================== +* TODO-FIND: complete the list of updated routes: + /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1') + REST API ------------ +-------- * Improved parsing of multiple numerical values in DICOM tags. https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6 - Maintenance -----------
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Thu Aug 29 15:41:42 2024 +0200 @@ -95,7 +95,8 @@ } else if (v.IsSequence()) { - s = "(sequence)"; + //s = "(sequence)"; + s = "(sequence) " + v.GetSequenceContent().toStyledString(); } else {
--- a/OrthancFramework/Sources/SQLite/Connection.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/Connection.h Thu Aug 29 15:41:42 2024 +0200 @@ -57,6 +57,7 @@ #endif #define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__) +#define SQLITE_FROM_HERE_DYNAMIC(sql) ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__, sql) namespace Orthanc {
--- a/OrthancFramework/Sources/SQLite/StatementId.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.cpp Thu Aug 29 15:41:42 2024 +0200 @@ -57,12 +57,24 @@ { } + Orthanc::SQLite::StatementId::StatementId(const char *file, + int line, + const std::string& statement) : + file_(file), + line_(line), + statement_(statement) + { + } + bool StatementId::operator< (const StatementId& other) const { if (line_ != other.line_) return line_ < other.line_; - return strcmp(file_, other.file_) < 0; + if (strcmp(file_, other.file_) < 0) + return true; + + return statement_ < other.statement_; } } }
--- a/OrthancFramework/Sources/SQLite/StatementId.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.h Thu Aug 29 15:41:42 2024 +0200 @@ -55,6 +55,7 @@ private: const char* file_; int line_; + std::string statement_; StatementId(); // Forbidden @@ -62,6 +63,10 @@ StatementId(const char* file, int line); + StatementId(const char* file, + int line, + const std::string& statement); + bool operator< (const StatementId& other) const; }; }
--- a/OrthancServer/CMakeLists.txt Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/CMakeLists.txt Thu Aug 29 15:41:42 2024 +0200 @@ -90,11 +90,16 @@ set(ORTHANC_SERVER_SOURCES ${CMAKE_SOURCE_DIR}/Sources/Database/BaseDatabaseWrapper.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/DatabaseLookup.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/GenericFind.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ICreateInstance.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/IGetChildrenMetadata.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResourceAndParent.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResources.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/SetOfResources.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/FindRequest.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/FindResponse.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/MainDicomTagsRegistry.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/OrthancIdentifiers.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/ResourcesContent.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/SQLiteDatabaseWrapper.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/StatelessDatabaseOperations.cpp @@ -119,6 +124,7 @@ ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestSystem.cpp ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp + ${CMAKE_SOURCE_DIR}/Sources/ResourceFinder.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraint.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DicomTagConstraint.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Thu Aug 29 15:41:42 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<MetadataType>::const_iterator it = source.GetMetadata().begin(); it != source.GetMetadata().end(); ++it) + { + target.add_retrieve_metadata(*it); + } + + for (std::set<DicomTag>::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<MetadataType>(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<MetadataType>(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<std::string>& resourcesId, std::list<std::string>* 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<std::string>::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<std::string>::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<FindResponse::Resource> 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<std::string>& 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(); + } }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Thu Aug 29 15:41:42 2024 +0200 @@ -93,6 +93,8 @@ virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE; + + virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Thu Aug 29 15:41:42 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)
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Thu Aug 29 15:41:42 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 {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/Graveyard/FindRefactoringForSQLite.cpp Thu Aug 29 15:41:42 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<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE + { +#if 0 + Compatibility::GenericFind find(*this); + find.Execute(response, request); +#else + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS FilteredResourcesIds"); + s.Run(); + } + + { + + LookupFormatter formatter; + + std::string sqlLookup; + LookupFormatter::Apply(sqlLookup, + formatter, + normalized, + request.GetLevel(), + request.GetLabels(), + request.GetLabelsConstraint(), + (request.HasLimits() ? request.GetLimitsCount() : 0)); // TODO: handles since and count + + { + // first create a temporary table that with the filtered and ordered results + sqlLookup = "CREATE TEMPORARY TABLE FilteredResourcesIds AS " + sqlLookup; + + SQLite::Statement statement(db_, SQLITE_FROM_HERE_DYNAMIC(sqlLookup), sqlLookup); + formatter.Bind(statement); + statement.Run(); + } + + { + // create the response item with the public ids only + SQLite::Statement statement(db_, SQLITE_FROM_HERE, "SELECT publicId FROM FilteredResourcesIds"); + formatter.Bind(statement); + + while (statement.Step()) + { + const std::string resourceId = statement.ColumnString(0); + response.Add(new FindResponse::Resource(request.GetLevel(), resourceId)); + } + } + + // request Each response content through INNER JOIN with the temporary table + if (request.IsRetrieveMainDicomTags()) + { + // TODO-FIND: handle the case where we request tags from multiple levels + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT publicId, tagGroup, tagElement, value FROM MainDicomTags AS tags " + " INNER JOIN FilteredResourcesIds ON tags.id = FilteredResourcesIds.internalId"); + formatter.Bind(statement); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddStringDicomTag(statement.ColumnInt(1), + statement.ColumnInt(2), + statement.ColumnString(3)); + } + } + + if (request.IsRetrieveChildrenIdentifiers()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, childLevel.publicId AS childPublicId " + "FROM Resources as currentLevel " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId " + " INNER JOIN Resources childLevel ON childLevel.parentId = currentLevel.internalId"); + formatter.Bind(statement); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddChildIdentifier(GetChildResourceType(request.GetLevel()), statement.ColumnString(1)); + } + } + + if (request.IsRetrieveParentIdentifier()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, parentLevel.publicId AS parentPublicId " + "FROM Resources as currentLevel " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId " + " INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + const std::string& parentId = statement.ColumnString(1); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).SetParentIdentifier(parentId); + } + } + + if (request.IsRetrieveMetadata()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, metadata.type, metadata.value " + "FROM Metadata " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Metadata.id"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddMetadata(static_cast<MetadataType>(statement.ColumnInt(1)), + statement.ColumnString(2)); + } + } + + if (request.IsRetrieveLabels()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, label " + "FROM Labels " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Labels.id"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddLabel(statement.ColumnString(1)); + } + } + + if (request.IsRetrieveAttachments()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, uuid, fileType, uncompressedSize, compressionType, compressedSize, " + " uncompressedMD5, compressedMD5 " + "FROM AttachedFiles " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = AttachedFiles.id"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + FileInfo attachment = FileInfo(statement.ColumnString(1), + static_cast<FileContentType>(statement.ColumnInt(2)), + statement.ColumnInt64(3), + statement.ColumnString(6), + static_cast<CompressionType>(statement.ColumnInt(4)), + statement.ColumnInt64(5), + statement.ColumnString(7)); + + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddAttachment(attachment); + }; + } + + // TODO-FIND: implement other responseContent: ResponseContent_ChildInstanceId, ResponseContent_ChildrenMetadata (later: ResponseContent_IsStable) + + } + +#endif + } +#endif +
--- a/OrthancServer/Resources/Orthanc.doxygen Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Resources/Orthanc.doxygen Thu Aug 29 15:41:42 2024 +0200 @@ -755,6 +755,7 @@ # Note: If this tag is empty the current directory is searched. INPUT = @CMAKE_SOURCE_DIR@/../OrthancFramework/Sources \ + @CMAKE_SOURCE_DIR@/Plugins/Engine \ @CMAKE_SOURCE_DIR@/Sources # This tag can be used to specify the character encoding of the source files
--- a/OrthancServer/Resources/RunCppCheck.sh Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Thu Aug 29 15:41:42 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
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Thu Aug 29 15:41:42 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<std::string>& 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
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Thu Aug 29 15:41:42 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<std::string>& 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; + } }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Thu Aug 29 15:41:42 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "GenericFind.h" + +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../../OrthancFramework/Sources/OrthancException.h" + +#include <stack> + + +namespace Orthanc +{ + namespace Compatibility + { + static bool IsRequestWithoutContraint(const FindRequest& request) + { + return (request.GetDicomTagConstraints().IsEmpty() && + request.GetMetadataConstraintsCount() == 0 && + request.GetLabels().empty() && + request.GetOrdering().empty()); + } + + static void GetChildren(std::list<int64_t>& target, + IDatabaseWrapper::ITransaction& transaction, + const std::list<int64_t>& resources) + { + target.clear(); + + for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + std::list<int64_t> tmp; + transaction.GetChildrenInternalId(tmp, *it); + target.splice(target.begin(), tmp); + } + } + + static void GetChildren(std::list<std::string>& target, + IDatabaseWrapper::ITransaction& transaction, + const std::list<int64_t>& resources) + { + target.clear(); + + for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + std::list<std::string> tmp; + transaction.GetChildrenPublicId(tmp, *it); + target.splice(target.begin(), tmp); + } + } + + static void GetChildrenIdentifiers(std::list<std::string>& children, + IDatabaseWrapper::ITransaction& transaction, + const OrthancIdentifiers& identifiers, + ResourceType topLevel, + ResourceType bottomLevel) + { + if (!IsResourceLevelAboveOrEqual(topLevel, bottomLevel) || + topLevel == bottomLevel) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::list<int64_t> currentResources; + ResourceType currentLevel; + + { + int64_t id; + if (!transaction.LookupResource(id, currentLevel, identifiers.GetLevel(topLevel)) || + currentLevel != topLevel) + { + throw OrthancException(ErrorCode_InexistentItem); + } + + currentResources.push_back(id); + } + + while (currentLevel != bottomLevel) + { + ResourceType nextLevel = GetChildResourceType(currentLevel); + if (nextLevel == bottomLevel) + { + GetChildren(children, transaction, currentResources); + } + else + { + std::list<int64_t> nextResources; + GetChildren(nextResources, transaction, currentResources); + currentResources.swap(nextResources); + } + + currentLevel = nextLevel; + } + } + + void GenericFind::ExecuteFind(std::list<std::string>& identifiers, + const 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<FindResponse::Resource> resource(new FindResponse::Resource(request.GetLevel(), internalId, identifier)); + + if (request.IsRetrieveParentIdentifier()) + { + assert(!parent.empty()); + resource->SetParentIdentifier(parent); + } + + if (request.IsRetrieveMainDicomTags()) + { + RetrieveMainDicomTags(*resource, level, internalId); + } + + if (request.IsRetrieveMetadata()) + { + transaction_.GetAllMetadata(resource->GetMetadata(level), internalId); + } + + { + const ResourceType topLevel = GetTopLevelOfInterest(request); + + int64_t currentId = internalId; + ResourceType currentLevel = level; + + while (currentLevel != topLevel) + { + int64_t parentId; + if (transaction_.LookupParent(parentId, currentId)) + { + currentId = parentId; + currentLevel = GetParentResourceType(currentLevel); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (request.GetParentSpecification(currentLevel).IsRetrieveMainDicomTags()) + { + RetrieveMainDicomTags(*resource, currentLevel, currentId); + } + + if (request.GetParentSpecification(currentLevel).IsRetrieveMetadata()) + { + transaction_.GetAllMetadata(resource->GetMetadata(currentLevel), currentId); + } + } + } + + if (capabilities.HasLabelsSupport() && + request.IsRetrieveLabels()) + { + transaction_.ListLabels(resource->GetLabels(), internalId); + } + + if (request.IsRetrieveAttachments()) + { + std::set<FileContentType> attachments; + transaction_.ListAvailableAttachments(attachments, internalId); + + for (std::set<FileContentType>::const_iterator it = attachments.begin(); it != attachments.end(); ++it) + { + FileInfo info; + int64_t revision; + if (transaction_.LookupAttachment(info, revision, internalId, *it) && + info.GetContentType() == *it) + { + resource->AddAttachment(info); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + + { + const ResourceType bottomLevel = GetBottomLevelOfInterest(request); + + std::list<int64_t> currentIds; + currentIds.push_back(internalId); + + ResourceType currentLevel = level; + + while (currentLevel != bottomLevel) + { + ResourceType childrenLevel = GetChildResourceType(currentLevel); + + if (request.GetChildrenSpecification(childrenLevel).IsRetrieveIdentifiers()) + { + for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it) + { + std::list<std::string> ids; + transaction_.GetChildrenPublicId(ids, *it); + + for (std::list<std::string>::const_iterator it2 = ids.begin(); it2 != ids.end(); ++it2) + { + resource->AddChildIdentifier(childrenLevel, *it2); + } + } + } + + const std::set<MetadataType>& metadata = request.GetChildrenSpecification(childrenLevel).GetMetadata(); + + for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it) + { + for (std::list<int64_t>::const_iterator it2 = currentIds.begin(); it2 != currentIds.end(); ++it2) + { + std::list<std::string> values; + transaction_.GetChildrenMetadata(values, *it2, *it); + + for (std::list<std::string>::const_iterator it3 = values.begin(); it3 != values.end(); ++it3) + { + resource->AddChildrenMetadataValue(childrenLevel, *it, *it3); + } + } + } + + const std::set<DicomTag>& mainDicomTags = request.GetChildrenSpecification(childrenLevel).GetMainDicomTags(); + + if (childrenLevel != bottomLevel || + !mainDicomTags.empty()) + { + std::list<int64_t> childrenIds; + + for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it) + { + std::list<int64_t> tmp; + transaction_.GetChildrenInternalId(tmp, *it); + + childrenIds.splice(childrenIds.end(), tmp); + } + + if (!mainDicomTags.empty()) + { + for (std::list<int64_t>::const_iterator it = childrenIds.begin(); it != childrenIds.end(); ++it) + { + DicomMap m; + transaction_.GetMainDicomTags(m, *it); + + for (std::set<DicomTag>::const_iterator it2 = mainDicomTags.begin(); it2 != mainDicomTags.end(); ++it2) + { + std::string value; + if (m.LookupStringValue(value, *it2, false /* no binary allowed */)) + { + resource->AddChildrenMainDicomTagValue(childrenLevel, *it2, value); + } + } + } + } + + currentIds = childrenIds; + } + else + { + currentIds.clear(); + } + + currentLevel = childrenLevel; + } + } + + if (request.IsRetrieveOneInstanceIdentifier() && + !request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()) + { + int64_t currentId = internalId; + ResourceType currentLevel = level; + + while (currentLevel != ResourceType_Instance) + { + std::list<int64_t> children; + transaction_.GetChildrenInternalId(children, currentId); + if (children.empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + currentId = children.front(); + currentLevel = GetChildResourceType(currentLevel); + } + } + + resource->AddChildIdentifier(ResourceType_Instance, transaction_.GetPublicId(currentId)); + } + + response.Add(resource.release()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h Thu Aug 29 15:41:42 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../IDatabaseWrapper.h" + +namespace Orthanc +{ + namespace Compatibility + { + class GenericFind : public boost::noncopyable + { + private: + IDatabaseWrapper::ITransaction& transaction_; + + void RetrieveMainDicomTags(FindResponse::Resource& target, + ResourceType level, + int64_t internalId); + + public: + explicit GenericFind(IDatabaseWrapper::ITransaction& transaction) : + transaction_(transaction) + { + } + + void ExecuteFind(std::list<std::string>& identifiers, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request); + + void ExecuteExpand(FindResponse& response, + const IDatabaseWrapper::Capabilities& capabilities, + const FindRequest& request, + const std::string& identifier); + }; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.cpp Thu Aug 29 15:41:42 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + +#include "MainDicomTagsRegistry.h" + +#include <cassert> + + +namespace Orthanc +{ + FindRequest::ParentSpecification& FindRequest::GetParentSpecification(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_) || + 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<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + } + + + void FindRequest::SetOrthancId(ResourceType level, + const std::string& id) + { + switch (level) + { + case ResourceType_Patient: + SetOrthancPatientId(id); + break; + + case ResourceType_Study: + SetOrthancStudyId(id); + break; + + case ResourceType_Series: + SetOrthancSeriesId(id); + break; + + case ResourceType_Instance: + SetOrthancInstanceId(id); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindRequest::SetOrthancPatientId(const std::string& id) + { + orthancIdentifiers_.SetPatientId(id); + } + + + void FindRequest::SetOrthancStudyId(const std::string& id) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + orthancIdentifiers_.SetStudyId(id); + } + } + + + void FindRequest::SetOrthancSeriesId(const std::string& id) + { + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + orthancIdentifiers_.SetSeriesId(id); + } + } + + + void FindRequest::SetOrthancInstanceId(const std::string& id) + { + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study || + level_ == ResourceType_Series) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + orthancIdentifiers_.SetInstanceId(id); + } + } + + + void FindRequest::SetLimits(uint64_t since, + uint64_t count) + { + hasLimits_ = true; + limitsSince_ = since; + limitsCount_ = count; + } + + + uint64_t FindRequest::GetLimitsSince() const + { + if (hasLimits_) + { + return limitsSince_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + uint64_t FindRequest::GetLimitsCount() const + { + if (hasLimits_) + { + return limitsCount_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindRequest::AddOrdering(const DicomTag& tag, + OrderingDirection direction) + { + ordering_.push_back(new Ordering(Key(tag), direction)); + } + + + void FindRequest::AddOrdering(MetadataType metadataType, + OrderingDirection direction) + { + ordering_.push_back(new Ordering(Key(metadataType), direction)); + } + + + void FindRequest::SetRetrieveParentIdentifier(bool retrieve) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + retrieveParentIdentifier_ = retrieve; + } + } + + + void FindRequest::SetRetrieveOneInstanceIdentifier(bool retrieve) + { + if (level_ == ResourceType_Instance) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + retrieveOneInstanceIdentifier_ = retrieve; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.h Thu Aug 29 15:41:42 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h" +#include "../Search/DatabaseConstraint.h" +#include "../Search/DicomTagConstraint.h" +#include "../Search/ISqlLookupFormatter.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" + +#include <deque> +#include <map> +#include <set> +#include <cassert> +#include <boost/shared_ptr.hpp> + +namespace Orthanc +{ + class MainDicomTagsRegistry; + + class FindRequest : public boost::noncopyable + { + public: + enum KeyType // used for ordering and filters + { + KeyType_DicomTag, + KeyType_Metadata + }; + + + enum OrderingDirection + { + OrderingDirection_Ascending, + OrderingDirection_Descending + }; + + + class Key + { + private: + KeyType type_; + DicomTag dicomTag_; + MetadataType metadata_; + + // TODO-FIND: to execute the query, we actually need: + // ResourceType level_; + // DicomTagType dicomTagType_; + // these are however only populated in StatelessDatabaseOperations -> we had to add the normalized lookup arg to ExecuteFind + + public: + explicit Key(const DicomTag& dicomTag) : + type_(KeyType_DicomTag), + dicomTag_(dicomTag), + metadata_(MetadataType_EndUser) + { + } + + explicit Key(MetadataType metadata) : + type_(KeyType_Metadata), + dicomTag_(0, 0), + metadata_(metadata) + { + } + + KeyType GetType() const + { + return type_; + } + + const DicomTag& GetDicomTag() const + { + assert(GetType() == KeyType_DicomTag); + return dicomTag_; + } + + MetadataType GetMetadataType() const + { + assert(GetType() == KeyType_Metadata); + return metadata_; + } + }; + + class Ordering : public boost::noncopyable + { + private: + OrderingDirection direction_; + Key key_; + + public: + Ordering(const Key& key, + OrderingDirection direction) : + direction_(direction), + key_(key) + { + } + + KeyType GetKeyType() const + { + return key_.GetType(); + } + + OrderingDirection GetDirection() const + { + return direction_; + } + + MetadataType GetMetadataType() const + { + return key_.GetMetadataType(); + } + + DicomTag GetDicomTag() const + { + return key_.GetDicomTag(); + } + }; + + + class ParentSpecification : public boost::noncopyable + { + private: + bool mainDicomTags_; + bool metadata_; + + public: + ParentSpecification() : + mainDicomTags_(false), + metadata_(false) + { + } + + void SetRetrieveMainDicomTags(bool retrieve) + { + mainDicomTags_ = retrieve; + } + + bool IsRetrieveMainDicomTags() const + { + return mainDicomTags_; + } + + void SetRetrieveMetadata(bool retrieve) + { + metadata_ = retrieve; + } + + bool IsRetrieveMetadata() const + { + return metadata_; + } + + bool IsOfInterest() const + { + return (mainDicomTags_ || metadata_); + } + }; + + + class ChildrenSpecification : public boost::noncopyable + { + private: + bool identifiers_; + std::set<MetadataType> metadata_; + std::set<DicomTag> mainDicomTags_; + + public: + ChildrenSpecification() : + identifiers_(false) + { + } + + void SetRetrieveIdentifiers(bool retrieve) + { + identifiers_ = retrieve; + } + + bool IsRetrieveIdentifiers() const + { + return identifiers_; + } + + void AddMetadata(MetadataType metadata) + { + metadata_.insert(metadata); + } + + const std::set<MetadataType>& GetMetadata() const + { + return metadata_; + } + + void AddMainDicomTag(const DicomTag& tag) + { + mainDicomTags_.insert(tag); + } + + const std::set<DicomTag>& GetMainDicomTags() const + { + return mainDicomTags_; + } + + bool IsOfInterest() const + { + return (identifiers_ || !metadata_.empty() || !mainDicomTags_.empty()); + } + }; + + + private: + // filter & ordering fields + ResourceType level_; // The level of the response (the filtering on tags, labels and metadata also happens at this level) + OrthancIdentifiers orthancIdentifiers_; // The response must belong to this Orthanc resources hierarchy + DatabaseConstraints dicomTagConstraints_; // All tags filters (note: the order is not important) + bool hasLimits_; + uint64_t limitsSince_; + uint64_t limitsCount_; + std::set<std::string> labels_; + LabelsConstraint labelsConstraint_; + + // TODO-FIND + std::deque<Ordering*> ordering_; // The ordering criteria (note: the order is important !) + std::deque<void*> /* 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> 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<Ordering*>& GetOrdering() const + { + return ordering_; + } + + void SetLabels(const std::set<std::string>& labels) + { + labels_ = labels; + } + + void AddLabel(const std::string& label) + { + labels_.insert(label); + } + + const std::set<std::string>& GetLabels() const + { + return labels_; + } + + LabelsConstraint GetLabelsConstraint() const + { + return labelsConstraint_; + } + + void SetLabelsConstraint(LabelsConstraint constraint) + { + labelsConstraint_ = constraint; + } + + void SetRetrieveMainDicomTags(bool retrieve) + { + retrieveMainDicomTags_ = retrieve; + } + + bool IsRetrieveMainDicomTags() const + { + return retrieveMainDicomTags_; + } + + void SetRetrieveMetadata(bool retrieve) + { + retrieveMetadata_ = retrieve; + } + + bool IsRetrieveMetadata() const + { + return retrieveMetadata_; + } + + void SetRetrieveLabels(bool retrieve) + { + retrieveLabels_ = retrieve; + } + + bool IsRetrieveLabels() const + { + return retrieveLabels_; + } + + void SetRetrieveAttachments(bool retrieve) + { + retrieveAttachments_ = retrieve; + } + + bool IsRetrieveAttachments() const + { + return retrieveAttachments_; + } + + void SetRetrieveParentIdentifier(bool retrieve); + + bool IsRetrieveParentIdentifier() const + { + return retrieveParentIdentifier_; + } + + ParentSpecification& GetParentSpecification(ResourceType level); + + const ParentSpecification& GetParentSpecification(ResourceType level) const + { + return const_cast<FindRequest&>(*this).GetParentSpecification(level); + } + + ChildrenSpecification& GetChildrenSpecification(ResourceType level); + + const ChildrenSpecification& GetChildrenSpecification(ResourceType level) const + { + return const_cast<FindRequest&>(*this).GetChildrenSpecification(level); + } + + void SetRetrieveOneInstanceIdentifier(bool retrieve); + + bool IsRetrieveOneInstanceIdentifier() const + { + return (retrieveOneInstanceIdentifier_ || + (level_ != ResourceType_Instance && + GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers())); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.cpp Thu Aug 29 15:41:42 2024 +0200 @@ -0,0 +1,733 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "FindResponse.h" + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../OrthancFramework/Sources/SerializationToolbox.h" + +#include <boost/lexical_cast.hpp> +#include <cassert> + + +namespace Orthanc +{ + class FindResponse::MainDicomTagsAtLevel::DicomValue : public boost::noncopyable + { + public: + enum ValueType + { + ValueType_String, + ValueType_Null + }; + + private: + ValueType type_; + std::string value_; + + public: + DicomValue(ValueType type, + const std::string& value) : + type_(type), + value_(value) + { + } + + ValueType GetType() const + { + return type_; + } + + const std::string& GetValue() const + { + switch (type_) + { + case ValueType_Null: + throw OrthancException(ErrorCode_BadSequenceOfCalls); + + case ValueType_String: + return value_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + }; + + + FindResponse::MainDicomTagsAtLevel::~MainDicomTagsAtLevel() + { + for (MainDicomTags::iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void FindResponse::MainDicomTagsAtLevel::AddNullDicomTag(uint16_t group, + uint16_t element) + { + const DicomTag tag(group, element); + + if (mainDicomTags_.find(tag) == mainDicomTags_.end()) + { + mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_Null, ""); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::MainDicomTagsAtLevel::AddStringDicomTag(uint16_t group, + uint16_t element, + const std::string& value) + { + const DicomTag tag(group, element); + + if (mainDicomTags_.find(tag) == mainDicomTags_.end()) + { + mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_String, value); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::MainDicomTagsAtLevel::Export(DicomMap& target) const + { + for (MainDicomTags::const_iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it) + { + assert(it->second != NULL); + + switch (it->second->GetType()) + { + case DicomValue::ValueType_String: + target.SetValue(it->first, it->second->GetValue(), false /* not binary */); + break; + + case DicomValue::ValueType_Null: + target.SetNullValue(it->first); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + FindResponse::ChildrenInformation::~ChildrenInformation() + { + for (MetadataValues::iterator it = metadataValues_.begin(); it != metadataValues_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + for (MainDicomTagValues::iterator it = mainDicomTagValues_.begin(); it != mainDicomTagValues_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void FindResponse::ChildrenInformation::AddIdentifier(const std::string& identifier) + { + if (identifiers_.find(identifier) == identifiers_.end()) + { + identifiers_.insert(identifier); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::ChildrenInformation::AddMetadataValue(MetadataType metadata, + const std::string& value) + { + MetadataValues::iterator found = metadataValues_.find(metadata); + + if (found == metadataValues_.end()) + { + std::set<std::string> s; + s.insert(value); + metadataValues_[metadata] = new std::set<std::string>(s); + } + else + { + assert(found->second != NULL); + found->second->insert(value); + } + } + + + void FindResponse::ChildrenInformation::GetMetadataValues(std::set<std::string>& values, + MetadataType metadata) const + { + MetadataValues::const_iterator found = metadataValues_.find(metadata); + + if (found == metadataValues_.end()) + { + values.clear(); + } + else + { + assert(found->second != NULL); + values = *found->second; + } + } + + + void FindResponse::ChildrenInformation::AddMainDicomTagValue(const DicomTag& tag, + const std::string& value) + { + MainDicomTagValues::iterator found = mainDicomTagValues_.find(tag); + + if (found == mainDicomTagValues_.end()) + { + std::set<std::string> s; + s.insert(value); + mainDicomTagValues_[tag] = new std::set<std::string>(s); + } + else + { + assert(found->second != NULL); + found->second->insert(value); + } + } + + + void FindResponse::ChildrenInformation::GetMainDicomTagValues(std::set<std::string>& values, + const DicomTag& tag) const + { + MainDicomTagValues::const_iterator found = mainDicomTagValues_.find(tag); + + if (found == mainDicomTagValues_.end()) + { + values.clear(); + } + else + { + assert(found->second != NULL); + values = *found->second; + } + } + + + FindResponse::ChildrenInformation& FindResponse::Resource::GetChildrenInformation(ResourceType level) + { + switch (level) + { + case ResourceType_Study: + if (level_ == ResourceType_Patient) + { + return childrenStudiesInformation_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + case ResourceType_Series: + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study) + { + return childrenSeriesInformation_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + case ResourceType_Instance: + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study || + level_ == ResourceType_Series) + { + return childrenInstancesInformation_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + FindResponse::MainDicomTagsAtLevel& FindResponse::Resource::GetMainDicomTagsAtLevel(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + switch (level) + { + case ResourceType_Patient: + return mainDicomTagsPatient_; + + case ResourceType_Study: + return mainDicomTagsStudy_; + + case ResourceType_Series: + return mainDicomTagsSeries_; + + case ResourceType_Instance: + return mainDicomTagsInstance_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindResponse::Resource::GetAllMainDicomTags(DicomMap& target) const + { + switch (level_) + { + // Don't reorder or add "break" below + case ResourceType_Instance: + mainDicomTagsInstance_.Export(target); + + case ResourceType_Series: + mainDicomTagsSeries_.Export(target); + + case ResourceType_Study: + mainDicomTagsStudy_.Export(target); + + case ResourceType_Patient: + mainDicomTagsPatient_.Export(target); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindResponse::Resource::AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value) + { + std::map<MetadataType, std::string>& m = GetMetadata(level); + + if (m.find(metadata) != m.end()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // Metadata already present + } + else + { + m[metadata] = value; + } + } + + + std::map<MetadataType, std::string>& FindResponse::Resource::GetMetadata(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + switch (level) + { + case ResourceType_Patient: + return metadataPatient_; + + case ResourceType_Study: + return metadataStudy_; + + case ResourceType_Series: + return metadataSeries_; + + case ResourceType_Instance: + return metadataInstance_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool FindResponse::Resource::LookupMetadata(std::string& value, + ResourceType level, + MetadataType metadata) const + { + const std::map<MetadataType, std::string>& m = GetMetadata(level); + + std::map<MetadataType, std::string>::const_iterator found = m.find(metadata); + + if (found == m.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + void FindResponse::Resource::SetParentIdentifier(const std::string& id) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else if (HasParentIdentifier()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parentIdentifier_.reset(new std::string(id)); + } + } + + + const std::string& FindResponse::Resource::GetParentIdentifier() const + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else if (HasParentIdentifier()) + { + return *parentIdentifier_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + bool FindResponse::Resource::HasParentIdentifier() const + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + return parentIdentifier_.get() != NULL; + } + } + + + void FindResponse::Resource::AddLabel(const std::string& label) + { + if (labels_.find(label) == labels_.end()) + { + labels_.insert(label); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::Resource::AddAttachment(const FileInfo& attachment) + { + if (attachments_.find(attachment.GetContentType()) == attachments_.end()) + { + attachments_[attachment.GetContentType()] = attachment; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + bool FindResponse::Resource::LookupAttachment(FileInfo& target, FileContentType type) const + { + std::map<FileContentType, FileInfo>::const_iterator it = attachments_.find(type); + if (it != attachments_.end()) + { + target = it->second; + return true; + } + else + { + return false; + } + } + + + const std::string& FindResponse::Resource::GetOneInstanceIdentifier() const + { + const std::set<std::string>& instances = GetChildrenInformation(ResourceType_Instance).GetIdentifiers(); + + if (instances.size() == 0) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // HasOneInstanceIdentifier() should have been called + } + else + { + return *instances.begin(); + } + } + + + static void DebugDicomMap(Json::Value& target, + const DicomMap& m) + { + DicomArray a(m); + for (size_t i = 0; i < a.GetSize(); i++) + { + if (a.GetElement(i).GetValue().IsNull()) + { + target[a.GetElement(i).GetTag().Format()] = Json::nullValue; + } + else if (a.GetElement(i).GetValue().IsString()) + { + target[a.GetElement(i).GetTag().Format()] = a.GetElement(i).GetValue().GetContent(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + static void DebugMetadata(Json::Value& target, + const std::map<MetadataType, std::string>& m) + { + target = Json::objectValue; + + for (std::map<MetadataType, std::string>::const_iterator it = m.begin(); it != m.end(); ++it) + { + target[EnumerationToString(it->first)] = it->second; + } + } + + + static void DebugAddAttachment(Json::Value& target, + const FileInfo& info) + { + Json::Value u = Json::arrayValue; + u.append(info.GetUuid()); + u.append(static_cast<Json::UInt64>(info.GetUncompressedSize())); + target[EnumerationToString(info.GetContentType())] = u; + } + + + static void DebugSetOfStrings(Json::Value& target, + const std::set<std::string>& values) + { + target = Json::arrayValue; + for (std::set<std::string>::const_iterator it = values.begin(); it != values.end(); ++it) + { + target.append(*it); + } + } + + + void FindResponse::Resource::DebugExport(Json::Value& target, + const FindRequest& request) const + { + target = Json::objectValue; + + target["Level"] = EnumerationToString(GetLevel()); + target["ID"] = GetIdentifier(); + + if (request.IsRetrieveParentIdentifier()) + { + target["ParentID"] = GetParentIdentifier(); + } + + if (request.IsRetrieveMainDicomTags()) + { + DicomMap m; + GetMainDicomTags(m, request.GetLevel()); + DebugDicomMap(target[EnumerationToString(GetLevel())]["MainDicomTags"], m); + } + + if (request.IsRetrieveMetadata()) + { + DebugMetadata(target[EnumerationToString(GetLevel())]["Metadata"], GetMetadata(request.GetLevel())); + } + + static const ResourceType levels[4] = { ResourceType_Patient, ResourceType_Study, ResourceType_Series, ResourceType_Instance }; + + for (size_t i = 0; i < 4; i++) + { + const char* level = EnumerationToString(levels[i]); + + if (levels[i] != request.GetLevel() && + IsResourceLevelAboveOrEqual(levels[i], request.GetLevel())) + { + if (request.GetParentSpecification(levels[i]).IsRetrieveMainDicomTags()) + { + DicomMap m; + GetMainDicomTags(m, levels[i]); + DebugDicomMap(target[level]["MainDicomTags"], m); + } + + if (request.GetParentSpecification(levels[i]).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<MetadataType>& metadata = request.GetChildrenSpecification(levels[i]).GetMetadata(); + for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it) + { + std::set<std::string> values; + GetChildrenInformation(levels[i]).GetMetadataValues(values, *it); + DebugSetOfStrings(target[level]["Metadata"][EnumerationToString(*it)], values); + } + + const std::set<DicomTag>& tags = request.GetChildrenSpecification(levels[i]).GetMainDicomTags(); + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + std::set<std::string> values; + GetChildrenInformation(levels[i]).GetMainDicomTagValues(values, *it); + DebugSetOfStrings(target[level]["MainDicomTags"][it->Format()], values); + } + } + } + + if (request.IsRetrieveLabels()) + { + DebugSetOfStrings(target["Labels"], labels_); + } + + if (request.IsRetrieveAttachments()) + { + Json::Value v = Json::objectValue; + for (std::map<FileContentType, FileInfo>::const_iterator it = attachments_.begin(); + it != attachments_.end(); ++it) + { + if (it->first != it->second.GetContentType()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + DebugAddAttachment(v, it->second); + } + } + target["Attachments"] = v; + } + + if (request.IsRetrieveOneInstanceIdentifier()) + { + target["OneInstance"] = GetOneInstanceIdentifier(); + } + } + + + FindResponse::~FindResponse() + { + for (size_t i = 0; i < items_.size(); i++) + { + assert(items_[i] != NULL); + delete items_[i]; + } + } + + + void FindResponse::Add(Resource* item /* takes ownership */) + { + std::unique_ptr<Resource> protection(item); + + if (item == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (!items_.empty() && + items_[0]->GetLevel() != item->GetLevel()) + { + throw OrthancException(ErrorCode_BadParameterType, "A find response must only contain resources of the same type"); + } + else + { + const std::string& id = item->GetIdentifier(); + + if (index_.find(id) == index_.end()) + { + items_.push_back(protection.release()); + index_[id] = item; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "This resource has already been added: " + id); + } + } + } + + + const FindResponse::Resource& FindResponse::GetResourceByIndex(size_t index) const + { + if (index >= items_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(items_[index] != NULL); + return *items_[index]; + } + } + + + FindResponse::Resource& FindResponse::GetResourceByIdentifier(const std::string& id) + { + Index::const_iterator found = index_.find(id); + + if (found == index_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + assert(found->second != NULL); + return *found->second; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.h Thu Aug 29 15:41:42 2024 +0200 @@ -0,0 +1,312 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" +#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" +#include "FindRequest.h" + +#include <boost/noncopyable.hpp> +#include <deque> +#include <map> +#include <set> +#include <list> + + +namespace Orthanc +{ + class FindResponse : public boost::noncopyable + { + private: + class MainDicomTagsAtLevel : public boost::noncopyable + { + private: + class DicomValue; + + typedef std::map<DicomTag, DicomValue*> MainDicomTags; + + MainDicomTags mainDicomTags_; + + public: + ~MainDicomTagsAtLevel(); + + void AddStringDicomTag(uint16_t group, + uint16_t element, + const std::string& value); + + // The "Null" value could be used in the future to indicate a + // value that is not available, typically a new "ExtraMainDicomTag" + void AddNullDicomTag(uint16_t group, + uint16_t element); + + void Export(DicomMap& target) const; + }; + + class ChildrenInformation : public boost::noncopyable + { + private: + typedef std::map<MetadataType, std::set<std::string>* > MetadataValues; + typedef std::map<DicomTag, std::set<std::string>* > MainDicomTagValues; + + std::set<std::string> identifiers_; + MetadataValues metadataValues_; + MainDicomTagValues mainDicomTagValues_; + + public: + ~ChildrenInformation(); + + void AddIdentifier(const std::string& identifier); + + const std::set<std::string>& GetIdentifiers() const + { + return identifiers_; + } + + void AddMetadataValue(MetadataType metadata, + const std::string& value); + + void GetMetadataValues(std::set<std::string>& values, + MetadataType metadata) const; + + void AddMainDicomTagValue(const DicomTag& tag, + const std::string& value); + + void GetMainDicomTagValues(std::set<std::string>& values, + const DicomTag& tag) const; + }; + + + public: + class Resource : public boost::noncopyable + { + private: + typedef std::map<MetadataType, std::list<std::string>*> ChildrenMetadata; + + ResourceType level_; + int64_t internalId_; // Internal ID of the resource in the database + std::string identifier_; + std::unique_ptr<std::string> parentIdentifier_; + MainDicomTagsAtLevel mainDicomTagsPatient_; + MainDicomTagsAtLevel mainDicomTagsStudy_; + MainDicomTagsAtLevel mainDicomTagsSeries_; + MainDicomTagsAtLevel mainDicomTagsInstance_; + std::map<MetadataType, std::string> metadataPatient_; + std::map<MetadataType, std::string> metadataStudy_; + std::map<MetadataType, std::string> metadataSeries_; + std::map<MetadataType, std::string> metadataInstance_; + ChildrenInformation childrenStudiesInformation_; + ChildrenInformation childrenSeriesInformation_; + ChildrenInformation childrenInstancesInformation_; + std::set<std::string> labels_; + std::map<FileContentType, FileInfo> attachments_; + + MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level); + + const MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level) const + { + return const_cast<Resource&>(*this).GetMainDicomTagsAtLevel(level); + } + + ChildrenInformation& GetChildrenInformation(ResourceType level); + + const ChildrenInformation& GetChildrenInformation(ResourceType level) const + { + return const_cast<Resource&>(*this).GetChildrenInformation(level); + } + + public: + Resource(ResourceType level, + int64_t internalId, + const std::string& identifier) : + level_(level), + internalId_(internalId), + identifier_(identifier) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + int64_t GetInternalId() const + { + return internalId_; + } + + const std::string& GetIdentifier() const + { + return identifier_; + } + + void SetParentIdentifier(const std::string& id); + + const std::string& GetParentIdentifier() const; + + bool HasParentIdentifier() const; + + void AddStringDicomTag(ResourceType level, + uint16_t group, + uint16_t element, + const std::string& value) + { + GetMainDicomTagsAtLevel(level).AddStringDicomTag(group, element, value); + } + + void AddNullDicomTag(ResourceType level, + uint16_t group, + uint16_t element) + { + GetMainDicomTagsAtLevel(level).AddNullDicomTag(group, element); + } + + void GetMainDicomTags(DicomMap& target, + ResourceType level) const + { + GetMainDicomTagsAtLevel(level).Export(target); + } + + void GetAllMainDicomTags(DicomMap& target) const; + + void AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value); + + std::map<MetadataType, std::string>& GetMetadata(ResourceType level); + + const std::map<MetadataType, std::string>& GetMetadata(ResourceType level) const + { + return const_cast<Resource&>(*this).GetMetadata(level); + } + + bool LookupMetadata(std::string& value, + ResourceType level, + MetadataType metadata) const; + + void AddChildIdentifier(ResourceType level, + const std::string& childId) + { + GetChildrenInformation(level).AddIdentifier(childId); + } + + const std::set<std::string>& GetChildrenIdentifiers(ResourceType level) const + { + return GetChildrenInformation(level).GetIdentifiers(); + } + + void AddChildrenMetadataValue(ResourceType level, + MetadataType metadata, + const std::string& value) + { + GetChildrenInformation(level).AddMetadataValue(metadata, value); + } + + void GetChildrenMetadataValues(std::set<std::string>& values, + ResourceType level, + MetadataType metadata) const + { + GetChildrenInformation(level).GetMetadataValues(values, metadata); + } + + void AddChildrenMainDicomTagValue(ResourceType level, + const DicomTag& tag, + const std::string& value) + { + GetChildrenInformation(level).AddMainDicomTagValue(tag, value); + } + + void GetChildrenMainDicomTagValues(std::set<std::string>& values, + ResourceType level, + const DicomTag& tag) const + { + GetChildrenInformation(level).GetMainDicomTagValues(values, tag); + } + + void AddLabel(const std::string& label); + + std::set<std::string>& GetLabels() + { + return labels_; + } + + const std::set<std::string>& GetLabels() const + { + return labels_; + } + + void AddAttachment(const FileInfo& attachment); + + bool LookupAttachment(FileInfo& target, + FileContentType type) const; + + const std::map<FileContentType, FileInfo>& GetAttachments() const + { + return attachments_; + } + + const std::string& GetOneInstanceIdentifier() const; + + bool HasOneInstanceIdentifier() const + { + return !GetChildrenIdentifiers(ResourceType_Instance).empty(); + } + + void DebugExport(Json::Value& target, + const FindRequest& request) const; + }; + + private: + typedef std::map<std::string, Resource*> Index; + + std::deque<Resource*> items_; + Index index_; + + public: + ~FindResponse(); + + void Add(Resource* item /* takes ownership */); + + size_t GetSize() const + { + return items_.size(); + } + + const Resource& GetResourceByIndex(size_t index) const; + + Resource& GetResourceByIdentifier(const std::string& id); + + const Resource& GetResourceByIdentifier(const std::string& id) const + { + return const_cast<FindResponse&>(*this).GetResourceByIdentifier(id); + } + + bool HasResource(const std::string& id) const + { + return (index_.find(id) != index_.end()); + } + }; +}
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Thu Aug 29 15:41:42 2024 +0200 @@ -29,6 +29,8 @@ #include "../ExportedResource.h" #include "../Search/ISqlLookupFormatter.h" #include "../ServerIndexChange.h" +#include "FindRequest.h" +#include "FindResponse.h" #include "IDatabaseListener.h" #include <list> @@ -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<std::string>& 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; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp Thu Aug 29 15:41:42 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "MainDicomTagsRegistry.h" + +#include "../ServerToolbox.h" + +namespace Orthanc +{ + void MainDicomTagsRegistry::LoadTags(ResourceType level) + { + { + const DicomTag* tags = NULL; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (registry_.find(tags[i]) == registry_.end()) + { + registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); + } + else + { + // These patient-level tags are copied in the study level + assert(level == ResourceType_Study && + (tags[i] == DICOM_TAG_PATIENT_ID || + tags[i] == DICOM_TAG_PATIENT_NAME || + tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); + } + } + } + + { + std::set<DicomTag> tags; + DicomMap::GetMainDicomTags(tags, level); + + for (std::set<DicomTag>::const_iterator + tag = tags.begin(); tag != tags.end(); ++tag) + { + if (registry_.find(*tag) == registry_.end()) + { + registry_[*tag] = TagInfo(level, DicomTagType_Main); + } + } + } + } + + + MainDicomTagsRegistry::MainDicomTagsRegistry() + { + LoadTags(ResourceType_Patient); + LoadTags(ResourceType_Study); + LoadTags(ResourceType_Series); + LoadTags(ResourceType_Instance); + } + + + void MainDicomTagsRegistry::LookupTag(ResourceType& level, + DicomTagType& type, + const DicomTag& tag) const + { + Registry::const_iterator it = registry_.find(tag); + + if (it == registry_.end()) + { + // Default values + level = ResourceType_Instance; + type = DicomTagType_Generic; + } + else + { + level = it->second.GetLevel(); + type = it->second.GetType(); + } + } + + + 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; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h Thu Aug 29 15:41:42 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../Search/DatabaseLookup.h" + +#include <boost/noncopyable.hpp> + + +namespace Orthanc +{ + class MainDicomTagsRegistry : public boost::noncopyable + { + private: + class TagInfo + { + private: + ResourceType level_; + DicomTagType type_; + + public: + TagInfo() + { + } + + TagInfo(ResourceType level, + DicomTagType type) : + level_(level), + type_(type) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + DicomTagType GetType() const + { + return type_; + } + }; + + typedef std::map<DicomTag, TagInfo> Registry; + + Registry registry_; + + void LoadTags(ResourceType level); + + public: + MainDicomTagsRegistry(); + + void LookupTag(ResourceType& level, + DicomTagType& type, + const DicomTag& tag) const; + + /** + * 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; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp Thu Aug 29 15:41:42 2024 +0200 @@ -0,0 +1,243 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + + +namespace Orthanc +{ + OrthancIdentifiers::OrthancIdentifiers(const OrthancIdentifiers& other) + { + if (other.HasPatientId()) + { + SetPatientId(other.GetPatientId()); + } + + if (other.HasStudyId()) + { + SetStudyId(other.GetStudyId()); + } + + if (other.HasSeriesId()) + { + SetSeriesId(other.GetSeriesId()); + } + + if (other.HasInstanceId()) + { + SetInstanceId(other.GetInstanceId()); + } + } + + + void OrthancIdentifiers::SetPatientId(const std::string& id) + { + if (HasPatientId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + patientId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetPatientId() const + { + if (HasPatientId()) + { + return *patientId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetStudyId(const std::string& id) + { + if (HasStudyId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + studyId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetStudyId() const + { + if (HasStudyId()) + { + return *studyId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetSeriesId(const std::string& id) + { + if (HasSeriesId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + seriesId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetSeriesId() const + { + if (HasSeriesId()) + { + return *seriesId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetInstanceId(const std::string& id) + { + if (HasInstanceId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + instanceId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetInstanceId() const + { + if (HasInstanceId()) + { + return *instanceId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + ResourceType OrthancIdentifiers::DetectLevel() const + { + if (HasPatientId() && + !HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Patient; + } + else if (HasPatientId() && + HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Study; + } + else if (HasPatientId() && + HasStudyId() && + HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Series; + } + else if (HasPatientId() && + HasStudyId() && + HasSeriesId() && + HasInstanceId()) + { + return ResourceType_Instance; + } + else + { + throw OrthancException(ErrorCode_InexistentItem); + } + } + + + void OrthancIdentifiers::SetLevel(ResourceType level, + const std::string& id) + { + switch (level) + { + case ResourceType_Patient: + SetPatientId(id); + break; + + case ResourceType_Study: + SetStudyId(id); + break; + + case ResourceType_Series: + SetSeriesId(id); + break; + + case ResourceType_Instance: + SetInstanceId(id); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + std::string OrthancIdentifiers::GetLevel(ResourceType level) const + { + switch (level) + { + case ResourceType_Patient: + return GetPatientId(); + + case ResourceType_Study: + return GetStudyId(); + + case ResourceType_Series: + return GetSeriesId(); + + case ResourceType_Instance: + return GetInstanceId(); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.h Thu Aug 29 15:41:42 2024 +0200 @@ -0,0 +1,93 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" + +#include <boost/noncopyable.hpp> +#include <string> + + +namespace Orthanc +{ + class OrthancIdentifiers : public boost::noncopyable + { + private: + std::unique_ptr<std::string> patientId_; + std::unique_ptr<std::string> studyId_; + std::unique_ptr<std::string> seriesId_; + std::unique_ptr<std::string> instanceId_; + + public: + OrthancIdentifiers() + { + } + + OrthancIdentifiers(const OrthancIdentifiers& other); + + void SetPatientId(const std::string& id); + + bool HasPatientId() const + { + return patientId_.get() != NULL; + } + + const std::string& GetPatientId() const; + + void SetStudyId(const std::string& id); + + bool HasStudyId() const + { + return studyId_.get() != NULL; + } + + const std::string& GetStudyId() const; + + void SetSeriesId(const std::string& id); + + bool HasSeriesId() const + { + return seriesId_.get() != NULL; + } + + const std::string& GetSeriesId() const; + + void SetInstanceId(const std::string& id); + + bool HasInstanceId() const + { + return instanceId_.get() != NULL; + } + + const std::string& GetInstanceId() const; + + ResourceType DetectLevel() const; + + void SetLevel(ResourceType level, + const std::string& id); + + std::string GetLevel(ResourceType level) const; + }; +}
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Thu Aug 29 15:41:42 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"
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Thu Aug 29 15:41:42 2024 +0200 @@ -300,141 +300,6 @@ } - class StatelessDatabaseOperations::MainDicomTagsRegistry : public boost::noncopyable - { - private: - class TagInfo - { - private: - ResourceType level_; - DicomTagType type_; - - public: - TagInfo() - { - } - - TagInfo(ResourceType level, - DicomTagType type) : - level_(level), - type_(type) - { - } - - ResourceType GetLevel() const - { - return level_; - } - - DicomTagType GetType() const - { - return type_; - } - }; - - typedef std::map<DicomTag, TagInfo> Registry; - - - Registry registry_; - - void LoadTags(ResourceType level) - { - { - const DicomTag* tags = NULL; - size_t size; - - ServerToolbox::LoadIdentifiers(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - if (registry_.find(tags[i]) == registry_.end()) - { - registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); - } - else - { - // These patient-level tags are copied in the study level - assert(level == ResourceType_Study && - (tags[i] == DICOM_TAG_PATIENT_ID || - tags[i] == DICOM_TAG_PATIENT_NAME || - tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); - } - } - } - - { - std::set<DicomTag> tags; - DicomMap::GetMainDicomTags(tags, level); - - for (std::set<DicomTag>::const_iterator - tag = tags.begin(); tag != tags.end(); ++tag) - { - if (registry_.find(*tag) == registry_.end()) - { - registry_[*tag] = TagInfo(level, DicomTagType_Main); - } - } - } - } - - void LookupTag(ResourceType& level, - DicomTagType& type, - const DicomTag& tag) const - { - Registry::const_iterator it = registry_.find(tag); - - if (it == registry_.end()) - { - // Default values - level = ResourceType_Instance; - type = DicomTagType_Generic; - } - else - { - level = it->second.GetLevel(); - type = it->second.GetType(); - } - } - - public: - MainDicomTagsRegistry() - { - LoadTags(ResourceType_Patient); - LoadTags(ResourceType_Study); - LoadTags(ResourceType_Series); - LoadTags(ResourceType_Instance); - } - - void NormalizeLookup(DatabaseConstraints& target, - const DatabaseLookup& source, - ResourceType queryLevel) const - { - target.Clear(); - - for (size_t i = 0; i < source.GetConstraintsCount(); i++) - { - ResourceType level; - DicomTagType type; - - LookupTag(level, type, source.GetConstraint(i).GetTag()); - - if (type == DicomTagType_Identifier || - type == DicomTagType_Main) - { - // Use the fact that patient-level tags are copied at the study level - if (level == ResourceType_Patient && - queryLevel != ResourceType_Patient) - { - level = ResourceType_Study; - } - - target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type)); - } - } - } - }; - - void StatelessDatabaseOperations::ReadWriteTransaction::LogChange(int64_t internalId, ChangeType changeType, ResourceType resourceType, @@ -708,285 +573,6 @@ } - bool StatelessDatabaseOperations::ExpandResource(ExpandedResource& target, - const std::string& publicId, - ResourceType level, - const std::set<DicomTag>& requestedTags, - ExpandResourceFlags expandFlags) - { - class Operations : public ReadOnlyOperationsT6< - bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, ExpandResourceFlags> - { - private: - bool hasLabelsSupport_; - - static bool LookupStringMetadata(std::string& result, - const std::map<MetadataType, std::string>& metadata, - MetadataType type) - { - std::map<MetadataType, std::string>::const_iterator found = metadata.find(type); - - if (found == metadata.end()) - { - return false; - } - else - { - result = found->second; - return true; - } - } - - - static bool LookupIntegerMetadata(int64_t& result, - const std::map<MetadataType, std::string>& metadata, - MetadataType type) - { - std::string s; - if (!LookupStringMetadata(s, metadata, type)) - { - return false; - } - - try - { - result = boost::lexical_cast<int64_t>(s); - return true; - } - catch (boost::bad_lexical_cast&) - { - return false; - } - } - - - public: - explicit Operations(bool hasLabelsSupport) : - hasLabelsSupport_(hasLabelsSupport) - { - } - - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - // Lookup for the requested resource - int64_t internalId; - ResourceType type; - std::string parent; - if (!transaction.LookupResourceAndParent(internalId, type, parent, tuple.get<2>()) || - type != tuple.get<3>()) - { - tuple.get<0>() = false; - } - else - { - ExpandedResource& target = tuple.get<1>(); - ExpandResourceFlags expandFlags = tuple.get<5>(); - - // Set information about the parent resource (if it exists) - if (type == ResourceType_Patient) - { - if (!parent.empty()) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - } - else - { - if (parent.empty()) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - target.parentId_ = parent; - } - - target.SetResource(type, tuple.get<2>()); - - if (expandFlags & ExpandResourceFlags_IncludeChildren) - { - // List the children resources - transaction.GetChildrenPublicId(target.childrenIds_, internalId); - } - - if (expandFlags & ExpandResourceFlags_IncludeMetadata) - { - // Extract the metadata - transaction.GetAllMetadata(target.metadata_, internalId); - - switch (type) - { - case ResourceType_Patient: - case ResourceType_Study: - break; - - case ResourceType_Series: - { - int64_t i; - if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Series_ExpectedNumberOfInstances)) - { - target.expectedNumberOfInstances_ = static_cast<int>(i); - target.status_ = EnumerationToString(transaction.GetSeriesStatus(internalId, i)); - } - else - { - target.expectedNumberOfInstances_ = -1; - target.status_ = EnumerationToString(SeriesStatus_Unknown); - } - - break; - } - - case ResourceType_Instance: - { - FileInfo attachment; - int64_t revision; // ignored - if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom)) - { - throw OrthancException(ErrorCode_InternalError); - } - - target.fileSize_ = static_cast<unsigned int>(attachment.GetUncompressedSize()); - target.fileUuid_ = attachment.GetUuid(); - - int64_t i; - if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Instance_IndexInSeries)) - { - target.indexInSeries_ = static_cast<int>(i); - } - else - { - target.indexInSeries_ = -1; - } - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - // check the main dicom tags list has not changed since the resource was stored - target.mainDicomTagsSignature_ = DicomMap::GetDefaultMainDicomTagsSignature(type); - LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature); - } - - if (expandFlags & ExpandResourceFlags_IncludeMainDicomTags) - { - // read all tags from DB - transaction.GetMainDicomTags(target.GetMainDicomTags(), internalId); - - // read all main sequences from DB - std::string serializedSequences; - if (LookupStringMetadata(serializedSequences, target.metadata_, MetadataType_MainDicomSequences)) - { - Json::Value jsonMetadata; - Toolbox::ReadJson(jsonMetadata, serializedSequences); - - assert(jsonMetadata["Version"].asInt() == 1); - target.GetMainDicomTags().FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */); - } - - // check if we have access to all requestedTags or if we must get tags from parents - const std::set<DicomTag>& requestedTags = tuple.get<4>(); - - if (requestedTags.size() > 0) - { - std::set<DicomTag> savedMainDicomTags; - - FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, target.mainDicomTagsSignature_); - - // read parent main dicom tags as long as we have not gathered all requested tags - ResourceType currentLevel = target.GetLevel(); - int64_t currentInternalId = internalId; - Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags); - - while ((target.missingRequestedTags_.size() > 0) - && currentLevel != ResourceType_Patient) - { - currentLevel = GetParentResourceType(currentLevel); - - int64_t currentParentId; - if (!transaction.LookupParent(currentParentId, currentInternalId)) - { - break; - } - - std::map<MetadataType, std::string> parentMetadata; - transaction.GetAllMetadata(parentMetadata, currentParentId); - - std::string parentMainDicomTagsSignature = DicomMap::GetDefaultMainDicomTagsSignature(currentLevel); - LookupStringMetadata(parentMainDicomTagsSignature, parentMetadata, MetadataType_MainDicomTagsSignature); - - std::set<DicomTag> parentSavedMainDicomTags; - FromDcmtkBridge::ParseListOfTags(parentSavedMainDicomTags, parentMainDicomTagsSignature); - - size_t previousMissingCount = target.missingRequestedTags_.size(); - Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags); - Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags); - - // read the parent tags from DB only if it reduces the number of missing tags - if (target.missingRequestedTags_.size() < previousMissingCount) - { - Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags); - - DicomMap parentTags; - transaction.GetMainDicomTags(parentTags, currentParentId); - - target.GetMainDicomTags().Merge(parentTags); - } - - currentInternalId = currentParentId; - } - } - } - - if ((expandFlags & ExpandResourceFlags_IncludeLabels) && - hasLabelsSupport_) - { - transaction.ListLabels(target.labels_, internalId); - } - - std::string tmp; - - if (LookupStringMetadata(tmp, target.metadata_, MetadataType_AnonymizedFrom)) - { - target.anonymizedFrom_ = tmp; - } - - if (LookupStringMetadata(tmp, target.metadata_, MetadataType_ModifiedFrom)) - { - target.modifiedFrom_ = tmp; - } - - if (type == ResourceType_Patient || - type == ResourceType_Study || - type == ResourceType_Series) - { - target.isStable_ = !transaction.GetTransactionContext().IsUnstableResource(type, internalId); - - if (LookupStringMetadata(tmp, target.metadata_, MetadataType_LastUpdate)) - { - target.lastUpdate_ = tmp; - } - } - else - { - target.isStable_ = false; - } - - tuple.get<0>() = true; - } - } - }; - - bool found; - Operations operations(db_.GetDatabaseCapabilities().HasLabelsSupport()); - operations.Apply(*this, found, target, publicId, level, requestedTags, expandFlags); - return found; - } - - void StatelessDatabaseOperations::GetAllMetadata(std::map<MetadataType, std::string>& target, const std::string& publicId, ResourceType level) @@ -1687,7 +1273,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 @@ -1977,77 +1564,6 @@ } - void StatelessDatabaseOperations::ApplyLookupResources(std::vector<std::string>& resourcesId, - std::vector<std::string>* instancesId, - const DatabaseLookup& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - uint32_t limit) - { - class Operations : public ReadOnlyOperationsT6<bool, const DatabaseConstraints&, ResourceType, - const std::set<std::string>&, LabelsConstraint, size_t> - { - private: - std::list<std::string> resourcesList_; - std::list<std::string> instancesList_; - - public: - const std::list<std::string>& GetResourcesList() const - { - return resourcesList_; - } - - const std::list<std::string>& GetInstancesList() const - { - return instancesList_; - } - - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - // TODO - CANDIDATE FOR "TransactionType_Implicit" - if (tuple.get<0>()) - { - transaction.ApplyLookupResources( - resourcesList_, &instancesList_, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>()); - } - else - { - transaction.ApplyLookupResources( - resourcesList_, NULL, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>()); - } - } - }; - - if (!labels.empty() && - !db_.GetDatabaseCapabilities().HasLabelsSupport()) - { - throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels"); - } - - for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) - { - ServerToolbox::CheckValidLabel(*it); - } - - DatabaseConstraints normalized; - - assert(mainDicomTagsRegistry_.get() != NULL); - mainDicomTagsRegistry_->NormalizeLookup(normalized, lookup, queryLevel); - - Operations operations; - operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, labels, labelsConstraint, limit); - - CopyListToVector(resourcesId, operations.GetResourcesList()); - - if (instancesId != NULL) - { - CopyListToVector(*instancesId, operations.GetInstancesList()); - } - } - - bool StatelessDatabaseOperations::DeleteResource(Json::Value& remainingAncestor, const std::string& uuid, ResourceType expectedType) @@ -2515,7 +2031,7 @@ catch (boost::bad_lexical_cast&) { LOG(ERROR) << "Cannot read the global sequence " - << boost::lexical_cast<std::string>(sequence_) << ", resetting it"; + << boost::lexical_cast<std::string>(sequence_) << ", resetting it"; oldValue = 0; } @@ -2814,8 +2330,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 +2456,7 @@ } bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount, - const std::string& patientId) + const std::string& patientId) { if (maximumPatientCount != 0) { @@ -3042,7 +2558,7 @@ }; if (maximumStorageMode == MaxStorageMode_Recycle - && (maximumStorageSize != 0 || maximumPatientCount != 0)) + && (maximumStorageSize != 0 || maximumPatientCount != 0)) { Operations operations(maximumStorageSize, maximumPatientCount); Apply(operations); @@ -3106,9 +2622,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 +2817,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 +2846,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 +3322,76 @@ boost::shared_lock<boost::shared_mutex> lock(mutex_); return db_.GetDatabaseCapabilities().HasLabelsSupport(); } + + + void StatelessDatabaseOperations::ExecuteFind(FindResponse& response, + const FindRequest& request) + { + class IntegratedFind : public ReadOnlyOperationsT3<FindResponse&, const FindRequest&, + const IDatabaseWrapper::Capabilities&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + class FindStage : public ReadOnlyOperationsT3<std::list<std::string>&, const 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<FindResponse&, const IDatabaseWrapper::Capabilities&, const FindRequest&, const std::string&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteExpand(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), 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<std::string> identifiers; + + FindStage find; + find.Apply(*this, identifiers, capabilities, request); + + ExpandStage expand; + + for (std::list<std::string>::const_iterator it = identifiers.begin(); it != identifiers.end(); ++it) + { + /** + * Not that the resource might have been deleted (as we are in + * another transaction). The database engine must ignore such + * error cases. + **/ + expand.Apply(*this, response, capabilities, request, *it); + } + } + } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Thu Aug 29 15:41:42 2024 +0200 @@ -25,8 +25,9 @@ #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../DicomInstanceOrigin.h" #include "IDatabaseWrapper.h" -#include "../DicomInstanceOrigin.h" +#include "MainDicomTagsRegistry.h" #include <boost/shared_ptr.hpp> #include <boost/thread/shared_mutex.hpp> @@ -38,91 +39,6 @@ class ParsedDicomFile; struct ServerIndexChange; - class ExpandedResource : public boost::noncopyable - { - private: - std::string id_; - ResourceType level_; - DicomMap tags_; // all main tags and main sequences from DB - - public: - std::string mainDicomTagsSignature_; - std::string parentId_; - std::list<std::string> childrenIds_; - std::map<MetadataType, std::string> metadata_; - std::string anonymizedFrom_; - std::string modifiedFrom_; - std::string lastUpdate_; - std::set<DicomTag> missingRequestedTags_; - - // for patients/studies/series - bool isStable_; - - // for series only - int expectedNumberOfInstances_; - std::string status_; - - // for instances only - size_t fileSize_; - std::string fileUuid_; - int indexInSeries_; - - // New in Orthanc 1.12.0 - std::set<std::string> labels_; - - public: - // TODO - Cleanup - ExpandedResource() : - level_(ResourceType_Instance), - isStable_(false), - expectedNumberOfInstances_(0), - fileSize_(0), - indexInSeries_(0) - { - } - - void SetResource(ResourceType level, - const std::string& id) - { - level_ = level; - id_ = id; - } - - const std::string& GetPublicId() const - { - return id_; - } - - ResourceType GetLevel() const - { - return level_; - } - - DicomMap& GetMainDicomTags() - { - return tags_; - } - - const DicomMap& GetMainDicomTags() const - { - return tags_; - } - }; - - enum ExpandResourceFlags - { - ExpandResourceFlags_None = 0, - 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) - }; - class StatelessDatabaseOperations : public boost::noncopyable { public: @@ -378,6 +294,28 @@ { transaction_.ListAllLabels(target); } + + void ExecuteFind(FindResponse& response, + const FindRequest& request, + const IDatabaseWrapper::Capabilities& capabilities) + { + transaction_.ExecuteFind(response, request, capabilities); + } + + void ExecuteFind(std::list<std::string>& identifiers, + const 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 +483,6 @@ private: - class MainDicomTagsRegistry; class Transaction; IDatabaseWrapper& db_; @@ -592,12 +529,6 @@ void Apply(IReadWriteOperations& operations); - bool ExpandResource(ExpandedResource& target, - const std::string& publicId, - ResourceType level, - const std::set<DicomTag>& requestedTags, - ExpandResourceFlags expandFlags); - void GetAllMetadata(std::map<MetadataType, std::string>& target, const std::string& publicId, ResourceType level); @@ -694,14 +625,6 @@ const std::string& publicId, ResourceType parentType); - void ApplyLookupResources(std::vector<std::string>& resourcesId, - std::vector<std::string>* instancesId, // Can be NULL if not needed - const DatabaseLookup& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - uint32_t limit); - bool DeleteResource(Json::Value& remainingAncestor /* out */, const std::string& uuid, ResourceType expectedType); @@ -798,5 +721,8 @@ const std::set<std::string>& labels); bool HasLabelsSupport(); + + void ExecuteFind(FindResponse& response, + const FindRequest& request); }; }
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Thu Aug 29 15:41:42 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,134 +40,48 @@ namespace Orthanc { - static void AddAnswer(DicomFindAnswers& answers, - ServerContext& context, - const std::string& publicId, - const std::string& instanceId, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson, - ResourceType level, - const DicomArray& query, - const std::list<DicomTag>& sequencesToReturn, - const std::string& defaultPrivateCreator, - const std::map<uint16_t, std::string>& privateCreators, - const std::string& retrieveAet, - bool allowStorageAccess) + static void CopySequence(ParsedDicomFile& dicom, + const DicomTag& tag, + const Json::Value& source, + const std::string& defaultPrivateCreator, + const std::map<uint16_t, std::string>& privateCreators) { - ExpandedResource resource; - std::set<DicomTag> requestedTags; - - query.GetTags(requestedTags); - requestedTags.erase(DICOM_TAG_QUERY_RETRIEVE_LEVEL); // this is not part of the answer - - // reuse ExpandResource to get missing tags and computed tags (ModalitiesInStudy ...). This code is therefore shared between C-Find, tools/find, list-resources and QIDO-RS - context.ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, - level, requestedTags, ExpandResourceFlags_IncludeMainDicomTags, allowStorageAccess); - - DicomMap result; + 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; - /** - * 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 < query.GetSize(); i++) - { - if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL) + for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++) { - // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052)) - result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue()); + Json::Value item; + Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short); + content.append(item); } - else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET) - { - // Do not include the encoding, this is handled by class ParsedDicomFile - } - else + + if (tag.IsPrivate()) { - const DicomTag& tag = query.GetElement(i).GetTag(); - const DicomValue* value = resource.GetMainDicomTags().TestAndGetValue(tag); + std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag.GetGroup()); - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) + if (found != privateCreators.end()) { - result.SetValue(tag, value->GetContent(), false); + dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str()); } else { - result.SetValue(tag, "", false); + dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator); } } + else + { + dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */); + } } - - 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 if (dicomAsJson == NULL) - { - CLOG(WARNING, DICOM) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled"; - answers.Add(result); - } - else - { - ParsedDicomFile dicom(result, GetDefaultDicomEncoding(), - true /* be permissive, cf. issue #136 */, defaultPrivateCreator, privateCreators); - - for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin(); - tag != sequencesToReturn.end(); ++tag) - { - 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<uint16_t, std::string>::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 */); - } - } - } - - answers.Add(dicom); - } } - bool OrthancFindRequestHandler::FilterQueryTag(std::string& value /* can be modified */, ResourceType level, const DicomTag& tag, @@ -237,86 +152,122 @@ } - class OrthancFindRequestHandler::LookupVisitor : public ServerContext::ILookupVisitor + namespace { - private: - DicomFindAnswers& answers_; - ServerContext& context_; - ResourceType level_; - const DicomMap& query_; - DicomArray queryAsArray_; - const std::list<DicomTag>& sequencesToReturn_; - std::string defaultPrivateCreator_; // the private creator to use if the group is not defined in the query itself - const std::map<uint16_t, std::string>& privateCreators_; // the private creators defined in the query itself - std::string retrieveAet_; - FindStorageAccessMode findStorageAccessMode_; + class LookupVisitorV2 : public ResourceFinder::IVisitor + { + private: + DicomFindAnswers& answers_; + DicomArray queryAsArray_; + const std::list<DicomTag>& sequencesToReturn_; + std::string defaultPrivateCreator_; // the private creator to use if the group is not defined in the query itself + const std::map<uint16_t, std::string>& privateCreators_; // the private creators defined in the query itself + std::string retrieveAet_; + + public: + LookupVisitorV2(DicomFindAnswers& answers, + const DicomMap& query, + const std::list<DicomTag>& sequencesToReturn, + const std::map<uint16_t, std::string>& privateCreators) : + answers_(answers), + queryAsArray_(query), + sequencesToReturn_(sequencesToReturn), + privateCreators_(privateCreators) + { + answers_.SetComplete(false); - public: - LookupVisitor(DicomFindAnswers& answers, - ServerContext& context, - ResourceType level, - const DicomMap& query, - const std::list<DicomTag>& sequencesToReturn, - const std::map<uint16_t, std::string>& privateCreators, - FindStorageAccessMode findStorageAccessMode) : - answers_(answers), - context_(context), - level_(level), - query_(query), - queryAsArray_(query), - sequencesToReturn_(sequencesToReturn), - privateCreators_(privateCreators), - findStorageAccessMode_(findStorageAccessMode) - { - 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 { - OrthancConfiguration::ReaderLock lock; - defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator(); - retrieveAet_ = lock.GetConfiguration().GetOrthancAET(); - } - } + 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(); - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - // Ask the "DICOM-as-JSON" attachment only if sequences are to - // be returned OR if "query_" contains non-main DICOM tags! - - DicomMap withoutSpecialTags; - withoutSpecialTags.Assign(query_); + 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); - // Check out "ComputeCounters()" - withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); - withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); - withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY); + if (value == NULL || + value->IsNull() || + value->IsBinary()) + { + result.SetValue(tag, "", false); + } + else + { + result.SetValue(tag, value->GetContent(), false); + } + } + } - // Check out "AddAnswer()" - withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); - withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL); - - return (!sequencesToReturn_.empty() || - !withoutSpecialTags.HasOnlyMainDicomTags()); - } - - virtual void MarkAsComplete() ORTHANC_OVERRIDE - { - answers_.SetComplete(true); - } + 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_); - virtual void Visit(const std::string& publicId, - const std::string& instanceId, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson) ORTHANC_OVERRIDE - { - AddAnswer(answers_, context_, publicId, instanceId, mainDicomTags, dicomAsJson, level_, queryAsArray_, sequencesToReturn_, - defaultPrivateCreator_, privateCreators_, retrieveAet_, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); - } - }; + for (std::list<DicomTag>::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, @@ -396,7 +347,6 @@ throw OrthancException(ErrorCode_NotImplemented); } - DicomArray query(*filteredInput); CLOG(INFO, DICOM) << "DICOM C-Find request at level: " << EnumerationToString(level); @@ -410,9 +360,12 @@ } } + std::set<DicomTag> requestedTags; + for (std::list<DicomTag>::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 +394,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; } @@ -483,11 +444,12 @@ * Run the query. **/ - size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_; - + ResourceFinder finder(level, false /* don't expand */); + finder.SetDatabaseLookup(lookup); + finder.AddRequestedTags(requestedTags); - LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators, context_.GetFindStorageAccessMode()); - context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit); + LookupVisitorV2 visitor(answers, *filteredInput, sequencesToReturn, privateCreators); + finder.Execute(visitor, context_); }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Thu Aug 29 15:41:42 2024 +0200 @@ -22,6 +22,8 @@ #include "../PrecompiledHeadersServer.h" +#include "../ResourceFinder.h" + #include "OrthancRestApi.h" #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h" @@ -127,79 +129,23 @@ } + 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, - ServerContext& context, - const std::list<std::string>& resources, - const std::map<std::string, std::string>& instancesIds, // optional: the id of an instance for each found resource. - const std::map<std::string, boost::shared_ptr<DicomMap> >& resourcesMainDicomTags, // optional: all tags read from DB for a resource (current level and upper levels) - const std::map<std::string, boost::shared_ptr<Json::Value> >& resourcesDicomAsJson, // optional: the dicom-as-json for each resource - ResourceType level, - bool expand, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess) - { - Json::Value answer = Json::arrayValue; - - for (std::list<std::string>::const_iterator - resource = resources.begin(); resource != resources.end(); ++resource) - { - if (expand) - { - Json::Value expanded; - - std::map<std::string, std::string>::const_iterator instanceId = instancesIds.find(*resource); - if (instanceId != instancesIds.end()) // if it is found in instancesIds, it is also in resourcesDicomAsJson and mainDicomTags - { - // reuse data already collected before (e.g during lookup) - std::map<std::string, boost::shared_ptr<DicomMap> >::const_iterator mainDicomTags = resourcesMainDicomTags.find(*resource); - std::map<std::string, boost::shared_ptr<Json::Value> >::const_iterator dicomAsJson = resourcesDicomAsJson.find(*resource); - - context.ExpandResource(expanded, *resource, - *(mainDicomTags->second.get()), - instanceId->second, - dicomAsJson->second.get(), - level, format, requestedTags, allowStorageAccess); - } - else - { - context.ExpandResource(expanded, *resource, level, format, requestedTags, allowStorageAccess); - } - - if (expanded.type() == Json::objectValue) - { - answer.append(expanded); - } - } - else - { - answer.append(*resource); - } - } - - output.AnswerJson(answer); - } - - - static void AnswerListOfResources(RestApiOutput& output, - ServerContext& context, - const std::list<std::string>& resources, - ResourceType level, - bool expand, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess) - { - std::map<std::string, std::string> unusedInstancesIds; - std::map<std::string, boost::shared_ptr<DicomMap> > unusedResourcesMainDicomTags; - std::map<std::string, boost::shared_ptr<Json::Value> > unusedResourcesDicomAsJson; - - AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess); - } - - template <enum ResourceType resourceType> static void ListResources(RestApiGetCall& call) { @@ -222,15 +168,18 @@ .SetHttpGetSample("https://orthanc.uclouvain.be/demo/" + resources + "?since=0&limit=2", true); return; } - - ServerIndex& index = OrthancRestApi::GetIndex(call); - ServerContext& context = OrthancRestApi::GetContext(call); - - std::list<std::string> result; + + // TODO-FIND: include the FindRequest options parsing in a method (parse from get-arguments and from post payload) + // TODO-FIND: support other values for expand like expand=MainDicomTags,Labels,Parent,SeriesStatus + const bool expand = (call.HasArgument("expand") && + call.GetBooleanArgument("expand", true)); std::set<DicomTag> requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); + ResourceFinder finder(resourceType, expand); + finder.AddRequestedTags(requestedTags); + if (call.HasArgument("limit") || call.HasArgument("since")) { @@ -248,19 +197,16 @@ call.FlattenUri()); } - size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", "")); - size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", "")); - index.GetAllUuids(result, resourceType, since, limit); + uint64_t since = boost::lexical_cast<uint64_t>(call.GetArgument("since", "")); + uint64_t limit = boost::lexical_cast<uint64_t>(call.GetArgument("limit", "")); + finder.SetLimitsSince(since); + finder.SetLimitsCount(limit); } - else - { - index.GetAllUuids(result, resourceType); - } - - AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true), - OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), - requestedTags, - true /* allowStorageAccess */); + + Json::Value answer; + finder.Execute(answer, OrthancRestApi::GetContext(call), + OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); } @@ -284,14 +230,17 @@ return; } - const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); - std::set<DicomTag> requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); + const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); + + ResourceFinder finder(resourceType, true /* expand */); + finder.AddRequestedTags(requestedTags); + finder.SetOrthancId(resourceType, call.GetUriComponent("id", "")); + Json::Value json; - if (OrthancRestApi::GetContext(call).ExpandResource( - json, call.GetUriComponent("id", ""), resourceType, format, requestedTags, true /* allowStorageAccess */)) + if (finder.ExecuteOneResource(json, OrthancRestApi::GetContext(call), format, false /* no "Metadata" field */)) { call.GetOutput().AnswerJson(json); } @@ -3082,70 +3031,6 @@ } - namespace - { - class FindVisitor : public ServerContext::ILookupVisitor - { - private: - bool isComplete_; - std::list<std::string> resources_; - FindStorageAccessMode findStorageAccessMode_; - - // cache the data we used during lookup and that we could reuse when building the answers - std::map<std::string, std::string> instancesIds_; // the id of an instance for each found resource. - std::map<std::string, boost::shared_ptr<DicomMap> > resourcesMainDicomTags_; // all tags read from DB for a resource (current level and upper levels) - std::map<std::string, boost::shared_ptr<Json::Value> > resourcesDicomAsJson_; // the dicom-as-json for a resource - - DicomToJsonFormat format_; - - public: - explicit FindVisitor(DicomToJsonFormat format, FindStorageAccessMode findStorageAccessMode) : - isComplete_(false), - findStorageAccessMode_(findStorageAccessMode), - format_(format) - { - } - - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - - virtual void MarkAsComplete() ORTHANC_OVERRIDE - { - isComplete_ = true; // Unused information as of Orthanc 1.5.0 - } - - virtual void Visit(const std::string& publicId, - const std::string& instanceId, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson) ORTHANC_OVERRIDE - { - resources_.push_back(publicId); - instancesIds_[publicId] = instanceId; - resourcesMainDicomTags_[publicId].reset(mainDicomTags.Clone()); - if (dicomAsJson != NULL) - { - resourcesDicomAsJson_[publicId].reset(new Json::Value(*dicomAsJson)); // keep our own copy because we might reuse it between lookup and answers - } - else - { - resourcesDicomAsJson_[publicId] = boost::shared_ptr<Json::Value>(); - } - } - - void Answer(RestApiOutput& output, - ServerContext& context, - ResourceType level, - bool expand, - const std::set<DicomTag>& requestedTags) const - { - AnswerListOfResources(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); - } - }; - } - - static void Find(RestApiPostCall& call) { static const char* const KEY_CASE_SENSITIVE = "CaseSensitive"; @@ -3260,72 +3145,81 @@ expand = request[KEY_EXPAND].asBool(); } - bool caseSensitive = false; - if (request.isMember(KEY_CASE_SENSITIVE)) - { - caseSensitive = request[KEY_CASE_SENSITIVE].asBool(); - } - - size_t limit = 0; + 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)) { - int tmp = request[KEY_LIMIT].asInt(); + int64_t tmp = request[KEY_LIMIT].asInt64(); if (tmp < 0) { throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer"); } - - limit = static_cast<size_t>(tmp); + else if (tmp != 0) // This is for compatibility with Orthanc 1.12.4 + { + finder.SetLimitsCount(static_cast<uint64_t>(tmp)); + } } - size_t since = 0; if (request.isMember(KEY_SINCE)) { - int tmp = request[KEY_SINCE].asInt(); + int64_t tmp = request[KEY_SINCE].asInt64(); if (tmp < 0) { throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer"); } - - since = static_cast<size_t>(tmp); + else + { + finder.SetLimitsSince(static_cast<uint64_t>(tmp)); + } } - std::set<DicomTag> requestedTags; + { + bool caseSensitive = false; + if (request.isMember(KEY_CASE_SENSITIVE)) + { + caseSensitive = request[KEY_CASE_SENSITIVE].asBool(); + } + + DatabaseLookup query; + + Json::Value::Members members = request[KEY_QUERY].getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + if (request[KEY_QUERY][members[i]].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Tag \"" + members[i] + "\" must be associated with a string"); + } + + const std::string value = request[KEY_QUERY][members[i]].asString(); + + if (!value.empty()) + { + // An empty string corresponds to an universal constraint, + // so we ignore it. This mimics the behavior of class + // "OrthancFindRequestHandler" + query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), + value, caseSensitive, true); + } + } + + finder.SetDatabaseLookup(query); + } if (request.isMember(KEY_REQUESTED_TAGS)) { + std::set<DicomTag> requestedTags; FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]); + finder.AddRequestedTags(requestedTags); } - ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); - - 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); - } - } - - std::set<std::string> labels; - if (request.isMember(KEY_LABELS)) // New in Orthanc 1.12.0 { for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++) @@ -3336,37 +3230,37 @@ } else { - labels.insert(request[KEY_LABELS][i].asString()); + finder.AddLabel(request[KEY_LABELS][i].asString()); } } } - LabelsConstraint labelsConstraint = LabelsConstraint_All; - + finder.SetLabelsConstraint(LabelsConstraint_All); + if (request.isMember(KEY_LABELS_CONSTRAINT)) { const std::string& s = request[KEY_LABELS_CONSTRAINT].asString(); if (s == "All") { - labelsConstraint = LabelsConstraint_All; + finder.SetLabelsConstraint(LabelsConstraint_All); } else if (s == "Any") { - labelsConstraint = LabelsConstraint_Any; + finder.SetLabelsConstraint(LabelsConstraint_Any); } else if (s == "None") { - labelsConstraint = LabelsConstraint_None; + finder.SetLabelsConstraint(LabelsConstraint_None); } else { throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\""); } } - - FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human), context.GetFindStorageAccessMode()); - context.Apply(visitor, query, level, labels, labelsConstraint, since, limit); - visitor.Answer(call.GetOutput(), context, level, expand, requestedTags); + + Json::Value answer; + finder.Execute(answer, context, format, false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); } } @@ -3395,37 +3289,21 @@ return; } - ServerIndex& index = OrthancRestApi::GetIndex(call); - ServerContext& context = OrthancRestApi::GetContext(call); + const bool expand = (!call.HasArgument("expand") || + // this "expand" is the only one to have a false default value to keep backward compatibility + call.GetBooleanArgument("expand", false)); + const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); std::set<DicomTag> requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); - std::list<std::string> a, b, c; - a.push_back(call.GetUriComponent("id", "")); - - ResourceType type = start; - while (type != end) - { - b.clear(); - - for (std::list<std::string>::const_iterator - it = a.begin(); it != a.end(); ++it) - { - index.GetChildren(c, *it); - b.splice(b.begin(), c); - } - - type = GetChildResourceType(type); - - a.clear(); - a.splice(a.begin(), b); - } - - 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 */); + ResourceFinder finder(end, expand); + finder.SetOrthancId(start, call.GetUriComponent("id", "")); + finder.AddRequestedTags(requestedTags); + + Json::Value answer; + finder.Execute(answer, OrthancRestApi::GetContext(call), format, false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); } @@ -3538,7 +3416,7 @@ const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); Json::Value resource; - if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags, true /* allowStorageAccess */)) + if (ExpandResource(resource, OrthancRestApi::GetContext(call), currentType, current, format, false)) { call.GetOutput().AnswerJson(resource); } @@ -3817,24 +3695,6 @@ } - static void AddMetadata(Json::Value& target, - ServerIndex& index, - const std::string& resource, - ResourceType level) - { - target = Json::objectValue; - - std::map<MetadataType, std::string> content; - index.GetAllMetadata(content, resource, level); - - for (std::map<MetadataType, std::string>::const_iterator - it = content.begin(); it != content.end(); ++it) - { - target[EnumerationToString(it->first)] = it->second; - } - } - - static void BulkContent(RestApiPostCall& call) { static const char* const LEVEL = "Level"; @@ -3972,15 +3832,8 @@ it = interest.begin(); it != interest.end(); ++it) { Json::Value item; - std::set<DicomTag> emptyRequestedTags; // not supported for bulk content - - if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */)) + if (ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata)) { - if (metadata) - { - AddMetadata(item[METADATA], index, *it, level); - } - answer.append(item); } } @@ -3996,16 +3849,10 @@ { ResourceType level; Json::Value item; - std::set<DicomTag> emptyRequestedTags; // not supported for bulk content if (index.LookupResourceType(level, *it) && - OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */)) + ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata)) { - if (metadata) - { - AddMetadata(item[METADATA], index, *it, level); - } - answer.append(item); } else
--- a/OrthancServer/Sources/OrthancWebDav.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Thu Aug 29 15:41:42 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,114 +77,108 @@ 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(); + } } - class OrthancWebDav::DicomIdentifiersVisitor : public ServerContext::ILookupVisitor + class OrthancWebDav::DicomIdentifiersVisitorV2 : public ResourceFinder::IVisitor { private: - ServerContext& context_; - bool isComplete_; - Collection& target_; - ResourceType level_; + bool isComplete_; + Collection& target_; public: - DicomIdentifiersVisitor(ServerContext& context, - Collection& target, - ResourceType level) : - context_(context), + explicit DicomIdentifiersVisitorV2(Collection& target) : isComplete_(false), - target_(target), - level_(level) + target_(target) { } - - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - + virtual void MarkAsComplete() ORTHANC_OVERRIDE { isComplete_ = true; // TODO } - 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 { - DicomTag tag(0, 0); - MetadataType timeMetadata; + DicomMap resourceTags; + resource.GetMainDicomTags(resourceTags, resource.GetLevel()); - switch (level_) + std::string uid; + bool hasUid; + + std::string time; + bool hasTime; + + switch (resource.GetLevel()) { case ResourceType_Study: - tag = DICOM_TAG_STUDY_INSTANCE_UID; - timeMetadata = MetadataType_LastUpdate; + hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_STUDY_INSTANCE_UID, false); + hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate); break; case ResourceType_Series: - tag = DICOM_TAG_SERIES_INSTANCE_UID; - timeMetadata = MetadataType_LastUpdate; + hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SERIES_INSTANCE_UID, false); + hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate); break; - + case ResourceType_Instance: - tag = DICOM_TAG_SOP_INSTANCE_UID; - timeMetadata = MetadataType_Instance_ReceptionDate; + 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); } - - std::string s; - if (mainDicomTags.LookupStringValue(s, tag, false) && - !s.empty()) + + if (hasUid && + !uid.empty()) { - std::unique_ptr<Resource> resource; + std::unique_ptr<Resource> item; - if (level_ == ResourceType_Instance) + if (resource.GetLevel() == ResourceType_Instance) { FileInfo info; - int64_t revision; // Ignored - if (context_.GetIndex().LookupAttachment(info, revision, publicId, FileContentType_Dicom)) + if (resource.LookupAttachment(info, FileContentType_Dicom)) { - std::unique_ptr<File> f(new File(s + ".dcm")); + std::unique_ptr<File> f(new File(uid + ".dcm")); f->SetMimeType(MimeType_Dicom); f->SetContentLength(info.GetUncompressedSize()); - resource.reset(f.release()); + item.reset(f.release()); } } else { - resource.reset(new Folder(s)); + item.reset(new Folder(uid)); } - if (resource.get() != NULL) + if (item.get() != NULL) { - boost::posix_time::ptime t; - LookupTime(t, context_, publicId, level_, timeMetadata); - resource->SetCreationTime(t); - target_.AddResource(resource.release()); + 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 + + class OrthancWebDav::DicomFileVisitorV2 : public ResourceFinder::IVisitor { private: ServerContext& context_; @@ -178,9 +187,9 @@ boost::posix_time::ptime& time_; public: - DicomFileVisitor(ServerContext& context, - std::string& target, - boost::posix_time::ptime& time) : + DicomFileVisitorV2(ServerContext& context, + std::string& target, + boost::posix_time::ptime& time) : context_(context), success_(false), target_(target), @@ -193,19 +202,12 @@ return success_; } - 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 { if (success_) { @@ -213,70 +215,18 @@ } else { - LookupTime(time_, context_, publicId, ResourceType_Instance, MetadataType_Instance_ReceptionDate); - context_.ReadDicom(target_, publicId); - success_ = true; - } - } - }; - - - class OrthancWebDav::OrthancJsonVisitor : public ServerContext::ILookupVisitor - { - private: - ServerContext& context_; - bool success_; - std::string& target_; - ResourceType level_; - - public: - OrthancJsonVisitor(ServerContext& context, - std::string& target, - ResourceType level) : - context_(context), - success_(false), - target_(target), - level_(level) - { - } - - bool IsSuccess() const - { - return success_; - } - - 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 - { - Json::Value resource; - std::set<DicomTag> emptyRequestedTags; // not supported for webdav - - if (context_.ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human, emptyRequestedTags, true /* allowStorageAccess */)) - { - if (success_) + std::string s; + if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_ReceptionDate)) { - success_ = false; // Two matches => Error + ParseTime(time_, s); } else { - target_ = resource.toStyledString(); + time_ = GetNow(); + } - // Replace UNIX newlines with DOS newlines - boost::replace_all(target_, "\n", "\r\n"); - - success_ = true; - } + context_.ReadDicom(target_, resource.GetIdentifier()); + success_ = true; } } }; @@ -955,7 +905,7 @@ std::string year_; std::string month_; - class Visitor : public ServerContext::ILookupVisitor + class Visitor : public ResourceFinder::IVisitor { private: std::list<std::string>& resources_; @@ -966,21 +916,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 +935,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 +971,7 @@ std::string year_; const Templates& templates_; - class Visitor : public ServerContext::ILookupVisitor + class Visitor : public ResourceFinder::IVisitor { private: std::set<std::string> months_; @@ -1036,20 +982,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 +1013,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<std::string>::const_iterator it = visitor.GetMonths().begin(); it != visitor.GetMonths().end(); ++it) @@ -1165,7 +1110,7 @@ }; - class OrthancWebDav::DicomDeleteVisitor : public ServerContext::ILookupVisitor + class OrthancWebDav::DicomDeleteVisitor : public ResourceFinder::IVisitor { private: ServerContext& context_; @@ -1179,22 +1124,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_); } }; @@ -1425,12 +1363,11 @@ { DatabaseLookup query; ResourceType level; - size_t limit = 0; // By default, no limits if (path.size() == 1) { level = ResourceType_Study; - limit = 0; // TODO - Should we limit here? + // TODO - Should we limit here? } else if (path.size() == 2) { @@ -1455,9 +1392,32 @@ return false; } - DicomIdentifiersVisitor visitor(context_, collection, level); - context_.Apply(visitor, query, level, 0 /* since */, limit); - + 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_); + return true; } else if (path[0] == BY_PATIENTS || @@ -1478,6 +1438,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 +1482,9 @@ 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(); + return GetOrthancJson(content, context_, ResourceType_Study, query); } else if (path.size() == 4 && path[3] == SERIES_INFO) @@ -1511,11 +1495,8 @@ 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; - return visitor.IsSuccess(); + return GetOrthancJson(content, context_, ResourceType_Series, query); } else if (path.size() == 4 && boost::ends_with(path[3], ".dcm")) @@ -1530,10 +1511,16 @@ 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; + + 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 @@ -1655,7 +1642,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
--- a/OrthancServer/Sources/OrthancWebDav.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/OrthancWebDav.h Thu Aug 29 15:41:42 2024 +0200 @@ -38,8 +38,8 @@ typedef std::map<ResourceType, std::string> Templates; class DicomDeleteVisitor; - class DicomFileVisitor; - class DicomIdentifiersVisitor; + class DicomFileVisitorV2; + class DicomIdentifiersVisitorV2; class InstancesOfSeries; class InternalNode; class ListOfResources; @@ -47,6 +47,7 @@ class ListOfStudiesByMonth; class ListOfStudiesByYear; class OrthancJsonVisitor; + class OrthancJsonVisitorV2; class ResourcesIndex; class RootNode; class SingleDicomResource;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ResourceFinder.cpp Thu Aug 29 15:41:42 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "PrecompiledHeadersServer.h" +#include "ResourceFinder.h" + +#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../OrthancFramework/Sources/Logging.h" +#include "../../OrthancFramework/Sources/OrthancException.h" +#include "../../OrthancFramework/Sources/SerializationToolbox.h" +#include "OrthancConfiguration.h" +#include "Search/DatabaseLookup.h" +#include "ServerContext.h" +#include "ServerIndex.h" + + +namespace Orthanc +{ + static bool IsComputedTag(const DicomTag& tag) + { + return (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES || + tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES || + tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES || + tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES || + tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES || + tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES || + tag == DICOM_TAG_SOP_CLASSES_IN_STUDY || + tag == DICOM_TAG_MODALITIES_IN_STUDY || + tag == DICOM_TAG_INSTANCE_AVAILABILITY); + } + + void ResourceFinder::ConfigureChildrenCountComputedTag(DicomTag tag, + ResourceType parentLevel, + ResourceType childLevel) + { + if (request_.GetLevel() == parentLevel) + { + requestedComputedTags_.insert(tag); + hasRequestedTags_ = true; + request_.GetChildrenSpecification(childLevel).SetRetrieveIdentifiers(true); + } + } + + + void ResourceFinder::InjectChildrenCountComputedTag(DicomMap& requestedTags, + DicomTag tag, + const FindResponse::Resource& resource, + ResourceType level) const + { + if (IsRequestedComputedTag(tag)) + { + const std::set<std::string>& children = resource.GetChildrenIdentifiers(level); + requestedTags.SetValue(tag, boost::lexical_cast<std::string>(children.size()), false); + } + } + + + void ResourceFinder::InjectComputedTags(DicomMap& requestedTags, + const FindResponse::Resource& resource) const + { + switch (resource.GetLevel()) + { + case ResourceType_Patient: + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, resource, ResourceType_Study); + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, resource, ResourceType_Series); + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, resource, ResourceType_Instance); + break; + + case ResourceType_Study: + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, resource, ResourceType_Series); + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, resource, ResourceType_Instance); + + if (IsRequestedComputedTag(DICOM_TAG_MODALITIES_IN_STUDY)) + { + std::set<std::string> modalities; + resource.GetChildrenMainDicomTagValues(modalities, ResourceType_Series, DICOM_TAG_MODALITY); + + std::string s; + Toolbox::JoinStrings(s, modalities, "\\"); + + requestedTags.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, s, false); + } + + if (IsRequestedComputedTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) + { + std::set<std::string> classes; + resource.GetChildrenMetadataValues(classes, ResourceType_Instance, MetadataType_Instance_SopClassUid); + + std::string s; + Toolbox::JoinStrings(s, classes, "\\"); + + requestedTags.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, s, false); + } + + break; + + case ResourceType_Series: + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, resource, ResourceType_Instance); + break; + + case ResourceType_Instance: + if (IsRequestedComputedTag(DICOM_TAG_INSTANCE_AVAILABILITY)) + { + requestedTags.SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false); + } + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + SeriesStatus ResourceFinder::GetSeriesStatus(uint32_t& expectedNumberOfInstances, + const FindResponse::Resource& resource) + { + if (resource.GetLevel() != ResourceType_Series) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + std::string s; + if (!resource.LookupMetadata(s, ResourceType_Series, MetadataType_Series_ExpectedNumberOfInstances) || + !SerializationToolbox::ParseUnsignedInteger32(expectedNumberOfInstances, s)) + { + return SeriesStatus_Unknown; + } + + std::set<std::string> values; + resource.GetChildrenMetadataValues(values, ResourceType_Instance, MetadataType_Instance_IndexInSeries); + + std::set<int64_t> instances; + + for (std::set<std::string>::const_iterator + it = values.begin(); it != values.end(); ++it) + { + int64_t index; + + if (!SerializationToolbox::ParseInteger64(index, *it)) + { + return SeriesStatus_Unknown; + } + + if (index <= 0 || + index > static_cast<int64_t>(expectedNumberOfInstances)) + { + // Out-of-range instance index + return SeriesStatus_Inconsistent; + } + + if (instances.find(index) != instances.end()) + { + // Twice the same instance index + return SeriesStatus_Inconsistent; + } + + instances.insert(index); + } + + if (instances.size() == static_cast<size_t>(expectedNumberOfInstances)) + { + return SeriesStatus_Complete; + } + else + { + return SeriesStatus_Missing; + } + } + + + void ResourceFinder::Expand(Json::Value& target, + const FindResponse::Resource& resource, + ServerIndex& index, + 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<std::string>& children = resource.GetChildrenIdentifiers(GetChildResourceType(resource.GetLevel())); + + Json::Value c = Json::arrayValue; + for (std::set<std::string>::const_iterator + it = children.begin(); it != children.end(); ++it) + { + c.append(*it); + } + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + target["Studies"] = c; + break; + + case ResourceType_Study: + target["Series"] = c; + break; + + case ResourceType_Series: + target["Instances"] = c; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + case ResourceType_Study: + break; + + case ResourceType_Series: + { + uint32_t expectedNumberOfInstances; + SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource); + + target["Status"] = EnumerationToString(status); + + static const char* const EXPECTED_NUMBER_OF_INSTANCES = "ExpectedNumberOfInstances"; + + if (status == SeriesStatus_Unknown) + { + target[EXPECTED_NUMBER_OF_INSTANCES] = Json::nullValue; + } + else + { + target[EXPECTED_NUMBER_OF_INSTANCES] = expectedNumberOfInstances; + } + + break; + } + + case ResourceType_Instance: + { + FileInfo info; + if (resource.LookupAttachment(info, FileContentType_Dicom)) + { + target["FileSize"] = static_cast<Json::UInt64>(info.GetUncompressedSize()); + target["FileUuid"] = info.GetUuid(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + + static const char* const INDEX_IN_SERIES = "IndexInSeries"; + + std::string s; + uint32_t 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<std::string>::const_iterator + it = resource.GetLabels().begin(); it != resource.GetLabels().end(); ++it) + { + labels.append(*it); + } + + target["Labels"] = labels; + } + + if (includeAllMetadata) // new in Orthanc 1.12.4 + { + const std::map<MetadataType, std::string>& m = resource.GetMetadata(resource.GetLevel()); + + Json::Value metadata = Json::objectValue; + + for (std::map<MetadataType, std::string>::const_iterator it = m.begin(); it != m.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second; + } + + target["Metadata"] = metadata; + } + } + + + void ResourceFinder::UpdateRequestLimits() + { + // By default, use manual paging + 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<DicomTag>& tags) + { + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + AddRequestedTag(*it); + } + } + + + static void InjectRequestedTags(DicomMap& requestedTags, + std::set<DicomTag>& missingTags /* out */, + const FindResponse::Resource& resource, + ResourceType level, + const std::set<DicomTag>& tags) + { + if (!tags.empty()) + { + DicomMap m; + resource.GetMainDicomTags(m, level); + + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + std::string value; + if (m.LookupStringValue(value, *it, false /* not binary */)) + { + requestedTags.SetValue(*it, value, false /* not binary */); + } + else + { + // This is the case where the Housekeeper should be run + missingTags.insert(*it); + } + } + } + } + + + static void ReadMissingTagsFromStorageArea(DicomMap& requestedTags, + ServerContext& context, + const FindRequest& request, + const FindResponse::Resource& resource, + const std::set<DicomTag>& missingTags) + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage)) + { + std::string missings; + FromDcmtkBridge::FormatListOfTags(missings, missingTags); + + LOG(WARNING) << "W001: Accessing DICOM tags from storage when accessing " + << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false) + << ": " << missings; + } + + std::string instancePublicId; + + if (request.IsRetrieveOneInstanceIdentifier()) + { + instancePublicId = resource.GetOneInstanceIdentifier(); + } + else if (request.GetLevel() == ResourceType_Instance) + { + instancePublicId = resource.GetIdentifier(); + } + else + { + FindRequest requestDicomAttachment(request.GetLevel()); + requestDicomAttachment.SetOrthancId(request.GetLevel(), resource.GetIdentifier()); + requestDicomAttachment.SetRetrieveOneInstanceIdentifier(true); + + FindResponse responseDicomAttachment; + context.GetIndex().ExecuteFind(responseDicomAttachment, requestDicomAttachment); + + if (responseDicomAttachment.GetSize() != 1 || + !responseDicomAttachment.GetResourceByIndex(0).HasOneInstanceIdentifier()) + { + throw OrthancException(ErrorCode_InexistentFile); + } + else + { + instancePublicId = responseDicomAttachment.GetResourceByIndex(0).GetOneInstanceIdentifier(); + } + } + + LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << instancePublicId; + + // TODO-FIND: What do we do if the DICOM has been removed since the request? + // Do we fail, or do we skip the resource? + + Json::Value tmpDicomAsJson; + context.ReadDicomAsJson(tmpDicomAsJson, instancePublicId, missingTags /* ignoreTagLength */); + + DicomMap tmpDicomMap; + tmpDicomMap.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/); + + for (std::set<DicomTag>::const_iterator it = missingTags.begin(); it != missingTags.end(); ++it) + { + assert(!requestedTags.HasTag(*it)); + if (tmpDicomMap.HasTag(*it)) + { + requestedTags.SetValue(*it, tmpDicomMap.GetValue(*it)); + } + else + { + requestedTags.SetNullValue(*it); // TODO-FIND: Is this compatible with Orthanc <= 1.12.3? + } + } + } + + + void ResourceFinder::Execute(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<DicomTag> missingTags = requestedTagsFromFileStorage_; + InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Patient, requestedPatientTags_); + InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Study, requestedStudyTags_); + InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Series, requestedSeriesTags_); + InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Instance, requestedInstanceTags_); + + if (!missingTags.empty()) + { + if (!allowStorageAccess_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Cannot add missing requested tags, as access to file storage is disallowed"); + } + else + { + ReadMissingTagsFromStorageArea(requestedTags, context, request_, resource, missingTags); + } + } + } + + bool match = true; + + if (lookup_.get() != NULL) + { + DicomMap tags; + resource.GetAllMainDicomTags(tags); + tags.Merge(requestedTags); + match = lookup_->IsMatch(tags); + } + + if (match) + { + if (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; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ResourceFinder.h Thu Aug 29 15:41:42 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "Database/FindRequest.h" +#include "Database/FindResponse.h" + +namespace Orthanc +{ + class DatabaseLookup; + class ServerContext; + class ServerIndex; + + class ResourceFinder : public boost::noncopyable + { + public: + class IVisitor : public boost::noncopyable + { + public: + virtual ~IVisitor() + { + } + + virtual void Apply(const FindResponse::Resource& resource, + 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<DatabaseLookup> lookup_; + bool isSimpleLookup_; + PagingMode pagingMode_; + bool hasLimitsSince_; + bool hasLimitsCount_; + uint64_t limitsSince_; + uint64_t limitsCount_; + bool expand_; + bool allowStorageAccess_; + bool hasRequestedTags_; + std::set<DicomTag> requestedPatientTags_; + std::set<DicomTag> requestedStudyTags_; + std::set<DicomTag> requestedSeriesTags_; + std::set<DicomTag> requestedInstanceTags_; + std::set<DicomTag> requestedTagsFromFileStorage_; + std::set<DicomTag> requestedComputedTags_; + + bool 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<DicomTag>& tags); + + void SetLabels(const std::set<std::string>& labels) + { + request_.SetLabels(labels); + } + + void AddLabel(const std::string& label) + { + request_.AddLabel(label); + } + + void SetLabelsConstraint(LabelsConstraint constraint) + { + request_.SetLabelsConstraint(constraint); + } + + void SetRetrieveOneInstanceIdentifier(bool retrieve) + { + request_.SetRetrieveOneInstanceIdentifier(retrieve); + } + + void 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; + }; +}
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp Thu Aug 29 15:41:42 2024 +0200 @@ -37,6 +37,7 @@ # include <OrthancException.h> #endif +#include <boost/lexical_cast.hpp> #include <cassert> @@ -284,4 +285,64 @@ return *constraints_[index]; } } + + + std::string DatabaseConstraints::Format() const + { + std::string s; + + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + const DatabaseConstraint& constraint = *constraints_[i]; + s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) + + ": " + constraint.GetTag().Format(); + + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + s += " == " + constraint.GetSingleValue(); + break; + + case ConstraintType_SmallerOrEqual: + s += " <= " + constraint.GetSingleValue(); + break; + + case ConstraintType_GreaterOrEqual: + s += " >= " + constraint.GetSingleValue(); + break; + + case ConstraintType_Wildcard: + s += " ~~ " + constraint.GetSingleValue(); + break; + + case ConstraintType_List: + { + s += " in [ "; + bool first = true; + for (size_t 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; + } }
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.h Thu Aug 29 15:41:42 2024 +0200 @@ -178,5 +178,7 @@ } const DatabaseConstraint& GetConstraint(size_t index) const; + + std::string Format() const; }; }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp Thu Aug 29 15:41:42 2024 +0200 @@ -215,6 +215,24 @@ } + static bool HasIntersection(const std::set<std::string>& expected, + const std::string& values) + { + std::vector<std::string> tokens; + Toolbox::TokenizeString(tokens, values, '\\'); + + for (size_t i = 0; i < tokens.size(); i++) + { + if (expected.find(tokens[i]) != expected.end()) + { + return true; + } + } + + return false; + } + + bool DicomTagConstraint::IsMatch(const std::string& value) const { NormalizedString source(value, caseSensitive_); @@ -224,7 +242,17 @@ case ConstraintType_Equal: { NormalizedString reference(GetValue(), caseSensitive_); - return source.GetValue() == reference.GetValue(); + + if (GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) + { + std::set<std::string> expected; + expected.insert(reference.GetValue()); + return HasIntersection(expected, source.GetValue()); + } + else + { + return source.GetValue() == reference.GetValue(); + } } case ConstraintType_SmallerOrEqual: @@ -251,17 +279,16 @@ case ConstraintType_List: { + std::set<std::string> references; + for (std::set<std::string>::const_iterator it = values_.begin(); it != values_.end(); ++it) { NormalizedString reference(*it, caseSensitive_); - if (source.GetValue() == reference.GetValue()) - { - return true; - } + references.insert(reference.GetValue()); } - return false; + return HasIntersection(references, source.GetValue()); } default: @@ -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<std::string> values; values.reserve(values_.size()); - + + isIdentical = true; + for (std::set<std::string>::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 {
--- a/OrthancServer/Sources/Search/DicomTagConstraint.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.h Thu Aug 29 15:41:42 2024 +0200 @@ -109,7 +109,8 @@ std::string Format() const; - DatabaseConstraint* ConvertToDatabaseConstraint(ResourceType level, + DatabaseConstraint* ConvertToDatabaseConstraint(bool& isIdentical /* out */, + ResourceType level, DicomTagType tagType) const; }; }
--- a/OrthancServer/Sources/ServerContext.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Thu Aug 29 15:41:42 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" @@ -68,12 +69,6 @@ namespace Orthanc { - static void ComputeStudyTags(ExpandedResource& resource, - ServerContext& context, - const std::string& studyPublicId, - const std::set<DicomTag>& requestedTags); - - static bool IsUncompressedTransferSyntax(DicomTransferSyntax transferSyntax) { return (transferSyntax == DicomTransferSyntax_LittleEndianImplicit || @@ -1541,191 +1536,6 @@ } - void ServerContext::Apply(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - size_t since, - size_t limit) - { - unsigned int databaseLimit = (queryLevel == ResourceType_Instance ? - limitFindInstances_ : limitFindResults_); - - std::vector<std::string> resources, instances; - const DicomTagConstraint* dicomModalitiesConstraint = NULL; - - bool hasModalitiesInStudyLookup = (queryLevel == ResourceType_Study && - lookup.GetConstraint(dicomModalitiesConstraint, DICOM_TAG_MODALITIES_IN_STUDY) && - ((dicomModalitiesConstraint->GetConstraintType() == ConstraintType_Equal && !dicomModalitiesConstraint->GetValue().empty()) || - (dicomModalitiesConstraint->GetConstraintType() == ConstraintType_List && !dicomModalitiesConstraint->GetValues().empty()))); - - std::unique_ptr<DatabaseLookup> fastLookup(lookup.Clone()); - - if (hasModalitiesInStudyLookup) - { - 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); - } - - bool complete = (databaseLimit == 0 || - resources.size() <= databaseLimit); - - LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size(); - - /** - * "resources" contains the Orthanc ID of the resource at level - * "queryLevel", "instances" contains one the Orthanc ID of one - * sample instance from this resource. - **/ - assert(resources.size() == instances.size()); - - size_t countResults = 0; - size_t skipped = 0; - - const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded(); - - for (size_t i = 0; i < instances.size(); i++) - { - // Optimization in Orthanc 1.5.1 - Don't read the full JSON from - // the disk if only "main DICOM tags" are to be returned - - boost::shared_ptr<Json::Value> dicomAsJson; - - bool hasOnlyMainDicomTags; - DicomMap dicom; - DicomMap allMainDicomTagsFromDB; - - if (!IsStorageAccessAllowedForAnswers(findStorageAccessMode_) - || fastLookup->HasOnlyMainDicomTags()) - { - // Case (1): The main DICOM tags, as stored in the database, - // are sufficient to look for match - - if (!GetIndex().GetAllMainDicomTags(allMainDicomTagsFromDB, instances[i])) - { - // The instance has been removed during the execution of the - // lookup, ignore it - continue; - } - - // New in Orthanc 1.6.0: Only keep the main DICOM tags at the - // level of interest for the query - switch (queryLevel) - { - // WARNING: Don't reorder cases below, and don't add "break" - case ResourceType_Instance: - dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Instance); - - case ResourceType_Series: - dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Series); - - case ResourceType_Study: - dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Study); - - case ResourceType_Patient: - dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Patient); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - hasOnlyMainDicomTags = true; - } - else - { - // Case (2): Need to read the "DICOM-as-JSON" attachment from - // the storage area - dicomAsJson.reset(new Json::Value); - ReadDicomAsJson(*dicomAsJson, instances[i]); - - dicom.FromDicomAsJson(*dicomAsJson); - - // This map contains the entire JSON, i.e. more than the main DICOM tags - hasOnlyMainDicomTags = false; - } - - if (fastLookup->IsMatch(dicom)) - { - bool isMatch = true; - - if (hasModalitiesInStudyLookup) - { - std::set<DicomTag> requestedTags; - requestedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); - ExpandedResource resource; - ComputeStudyTags(resource, *this, resources[i], requestedTags); - - std::vector<std::string> modalities; - Toolbox::TokenizeString(modalities, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY).GetContent(), '\\'); - bool hasAtLeastOneModalityMatching = false; - for (size_t m = 0; m < modalities.size(); m++) - { - hasAtLeastOneModalityMatching |= dicomModalitiesConstraint->IsMatch(modalities[m]); - } - - isMatch = isMatch && hasAtLeastOneModalityMatching; - // copy the value of ModalitiesInStudy such that it can be reused to build the answer - allMainDicomTagsFromDB.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY)); - } - - if (isMatch) - { - if (skipped < since) - { - skipped++; - } - else if (limit != 0 && - countResults >= limit) - { - // Too many results, don't mark as complete - complete = false; - break; - } - else - { - if (IsStorageAccessAllowedForAnswers(findStorageAccessMode_) && - dicomAsJson.get() == NULL && - isDicomAsJsonNeeded) - { - dicomAsJson.reset(new Json::Value); - ReadDicomAsJson(*dicomAsJson, instances[i]); - } - - if (hasOnlyMainDicomTags) - { - // This is Case (1): The variable "dicom" only contains the main DICOM tags - visitor.Visit(resources[i], instances[i], allMainDicomTagsFromDB, dicomAsJson.get()); - } - else - { - // Remove the non-main DICOM tags from "dicom" if Case (2) - // was used, for consistency with Case (1) - - DicomMap mainDicomTags; - mainDicomTags.ExtractMainDicomTags(dicom); - visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get()); - } - - countResults ++; - } - } - } - } - - if (complete) - { - visitor.MarkAsComplete(); - } - - LOG(INFO) << "Number of matching resources: " << countResults; - } - bool ServerContext::LookupOrReconstructMetadata(std::string& target, const std::string& publicId, ResourceType level, @@ -2133,570 +1943,6 @@ isUnknownSopClassAccepted_ = accepted; } - - static void SerializeExpandedResource(Json::Value& target, - const ExpandedResource& resource, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags) - { - 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()) - { - case ResourceType_Patient: - case ResourceType_Study: - case ResourceType_Series: - { - Json::Value c = Json::arrayValue; - - for (std::list<std::string>::const_iterator - it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it) - { - c.append(*it); - } - - if (resource.GetLevel() == ResourceType_Patient) - { - target["Studies"] = c; - } - else if (resource.GetLevel() == ResourceType_Study) - { - target["Series"] = c; - } - else - { - target["Instances"] = c; - } - break; - } - - case ResourceType_Instance: - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - switch (resource.GetLevel()) - { - 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["FileSize"] = static_cast<unsigned int>(resource.fileSize_); - target["FileUuid"] = resource.fileUuid_; - - if (resource.indexInSeries_ < 0) - { - target["IndexInSeries"] = Json::nullValue; - } - else - { - target["IndexInSeries"] = resource.indexInSeries_; - } - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - 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 (!resource.lastUpdate_.empty()) - { - target["LastUpdate"] = resource.lastUpdate_; - } - } - - // 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); - - } - - { - Json::Value labels = Json::arrayValue; - - for (std::set<std::string>::const_iterator it = resource.labels_.begin(); it != resource.labels_.end(); ++it) - { - labels.append(*it); - } - - target["Labels"] = labels; - } - } - - - static void ComputeInstanceTags(ExpandedResource& resource, - ServerContext& context, - const std::string& instancePublicId, - const std::set<DicomTag>& requestedTags) - { - if (requestedTags.count(DICOM_TAG_INSTANCE_AVAILABILITY) > 0) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false); - resource.missingRequestedTags_.erase(DICOM_TAG_INSTANCE_AVAILABILITY); - } - } - - - static void ComputeSeriesTags(ExpandedResource& resource, - ServerContext& context, - const std::string& seriesPublicId, - const std::set<DicomTag>& requestedTags) - { - if (requestedTags.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0) - { - ServerIndex& index = context.GetIndex(); - std::list<std::string> instances; - - index.GetChildren(instances, seriesPublicId); - - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, - boost::lexical_cast<std::string>(instances.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); - } - } - - static void ComputeStudyTags(ExpandedResource& resource, - ServerContext& context, - const std::string& studyPublicId, - const std::set<DicomTag>& requestedTags) - { - ServerIndex& index = context.GetIndex(); - std::list<std::string> series; - std::list<std::string> instances; - - bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0; - bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0; - bool hasModalitiesInStudy = requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0; - bool hasSopClassesInStudy = requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0; - - index.GetChildren(series, studyPublicId); - - if (hasModalitiesInStudy) - { - std::set<std::string> values; - - for (std::list<std::string>::const_iterator - it = series.begin(); it != series.end(); ++it) - { - DicomMap tags; - index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series); - - const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); - - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) - { - values.insert(value->GetContent()); - } - } - - std::string modalities; - Toolbox::JoinStrings(modalities, values, "\\"); - - resource.GetMainDicomTags().SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false); - resource.missingRequestedTags_.erase(DICOM_TAG_MODALITIES_IN_STUDY); - } - - if (hasNbRelatedSeries) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, - boost::lexical_cast<std::string>(series.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); - } - - if (hasNbRelatedInstances || hasSopClassesInStudy) - { - for (std::list<std::string>::const_iterator - it = series.begin(); it != series.end(); ++it) - { - std::list<std::string> seriesInstancesIds; - index.GetChildren(seriesInstancesIds, *it); - - instances.splice(instances.end(), seriesInstancesIds); - } - - if (hasNbRelatedInstances) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, - boost::lexical_cast<std::string>(instances.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); - } - - if (hasSopClassesInStudy) - { - std::set<std::string> values; - - for (std::list<std::string>::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - std::string value; - - if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid)) - { - values.insert(value); - } - } - - if (values.size() > 0) - { - std::string sopClassUids; - Toolbox::JoinStrings(sopClassUids, values, "\\"); - resource.GetMainDicomTags().SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false); - } - - resource.missingRequestedTags_.erase(DICOM_TAG_SOP_CLASSES_IN_STUDY); - } - } - } - - static void ComputePatientTags(ExpandedResource& resource, - ServerContext& context, - const std::string& patientPublicId, - const std::set<DicomTag>& requestedTags) - { - ServerIndex& index = context.GetIndex(); - - std::list<std::string> studies; - std::list<std::string> series; - std::list<std::string> instances; - - bool hasNbRelatedStudies = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) > 0; - bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) > 0; - bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) > 0; - - index.GetChildren(studies, patientPublicId); - - if (hasNbRelatedStudies) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, - boost::lexical_cast<std::string>(studies.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); - } - - if (hasNbRelatedSeries || hasNbRelatedInstances) - { - for (std::list<std::string>::const_iterator - it = studies.begin(); it != studies.end(); ++it) - { - std::list<std::string> thisSeriesIds; - index.GetChildren(thisSeriesIds, *it); - series.splice(series.end(), thisSeriesIds); - } - - if (hasNbRelatedSeries) - { - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, - boost::lexical_cast<std::string>(series.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); - } - } - - if (hasNbRelatedInstances) - { - for (std::list<std::string>::const_iterator - it = series.begin(); it != series.end(); ++it) - { - std::list<std::string> thisInstancesIds; - index.GetChildren(thisInstancesIds, *it); - instances.splice(instances.end(), thisInstancesIds); - } - - resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, - boost::lexical_cast<std::string>(instances.size()), false); - resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); - } - } - - - static void ComputeTags(ExpandedResource& resource, - ServerContext& context, - const std::string& resourceId, - ResourceType level, - const std::set<DicomTag>& requestedTags) - { - if (level == ResourceType_Patient - && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Patient)) - { - ComputePatientTags(resource, context, resourceId, requestedTags); - } - - if (level == ResourceType_Study - && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Study)) - { - ComputeStudyTags(resource, context, resourceId, requestedTags); - } - - if (level == ResourceType_Series - && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Series)) - { - ComputeSeriesTags(resource, context, resourceId, requestedTags); - } - - if (level == ResourceType_Instance - && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Instance)) - { - ComputeInstanceTags(resource, context, resourceId, requestedTags); - } - } - - bool ServerContext::ExpandResource(Json::Value& target, - const std::string& publicId, - ResourceType level, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess) - { - std::string unusedInstanceId; - Json::Value* unusedDicomAsJson = NULL; - DicomMap unusedMainDicomTags; - - return ExpandResource(target, publicId, unusedMainDicomTags, unusedInstanceId, unusedDicomAsJson, level, format, requestedTags, allowStorageAccess); - } - - bool ServerContext::ExpandResource(Json::Value& target, - const std::string& publicId, - const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) - const std::string& instanceId, // optional: the id of an instance for the resource (if already available) - const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource (if already available) - ResourceType level, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess) - { - ExpandedResource resource; - - if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_Default, allowStorageAccess)) - { - SerializeExpandedResource(target, resource, format, requestedTags); - return true; - } - - return false; - } - - bool ServerContext::ExpandResource(ExpandedResource& resource, - const std::string& publicId, - const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) - const std::string& instanceId, // optional: the id of an instance for the resource (if already available) - const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource (if already available) - ResourceType level, - const std::set<DicomTag>& requestedTags, - ExpandResourceFlags expandFlags, - bool allowStorageAccess) - { - // first try to get the tags from what is already available - - if ((expandFlags & ExpandResourceFlags_IncludeMainDicomTags) && - mainDicomTags.GetSize() > 0 && - dicomAsJson != NULL) - { - - resource.GetMainDicomTags().Merge(mainDicomTags); - - if (dicomAsJson->isObject()) - { - resource.GetMainDicomTags().FromDicomAsJson(*dicomAsJson); - } - - std::set<DicomTag> retrievedTags; - std::set<DicomTag> missingTags; - resource.GetMainDicomTags().GetTags(retrievedTags); - - Toolbox::GetMissingsFromSet(missingTags, requestedTags, retrievedTags); - - // if all possible tags have been read, no need to get them from DB anymore - if (missingTags.size() > 0 && DicomMap::HasOnlyComputedTags(missingTags)) - { - resource.missingRequestedTags_ = missingTags; - ComputeTags(resource, *this, publicId, level, requestedTags); - return true; - } - else if (missingTags.size() == 0) - { - expandFlags = static_cast<ExpandResourceFlags>(expandFlags & ~ExpandResourceFlags_IncludeMainDicomTags); - } - - if (missingTags.size() == 0 && expandFlags == ExpandResourceFlags_None) // we have already retrieved anything we need - { - return true; - } - } - - if (expandFlags != ExpandResourceFlags_None && - GetIndex().ExpandResource(resource, publicId, level, requestedTags, - static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeMetadata))) // we always need the metadata to get the mainDicomTagsSignature - { - // check the main dicom tags list has not changed since the resource was stored - if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.GetLevel())) - { - OrthancConfiguration::ReaderLock lock; - if (lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb)) - { - LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.GetLevel(), false , false) - << " has been stored with another version of Main Dicom Tags list, you should POST to /" - << Orthanc::GetResourceTypeText(resource.GetLevel(), true, false) - << "/" << resource.GetPublicId() - << "/reconstruct to update the list of tags saved in DB. Some MainDicomTags might be missing from this answer."; - } - } - - // possibly merge missing requested tags from dicom-as-json - if (allowStorageAccess && - !resource.missingRequestedTags_.empty() && - !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_)) - { - OrthancConfiguration::ReaderLock lock; - if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage)) - { - std::set<DicomTag> missingTags; - Toolbox::AppendSets(missingTags, resource.missingRequestedTags_); - for (std::set<DicomTag>::const_iterator it = resource.missingRequestedTags_.begin(); it != resource.missingRequestedTags_.end(); ++it) - { - if (DicomMap::IsComputedTag(*it)) - { - missingTags.erase(*it); - } - } - - 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 instanceId_ = instanceId; - DicomMap tagsFromJson; - - if (dicomAsJson == NULL) - { - if (instanceId_.empty()) - { - if (level == ResourceType_Instance) - { - instanceId_ = publicId; - } - else - { - std::list<std::string> instancesIds; - GetIndex().GetChildInstances(instancesIds, publicId); - if (instancesIds.size() < 1) - { - throw OrthancException(ErrorCode_InternalError, "ExpandResource: no instances found"); - } - instanceId_ = instancesIds.front(); - } - } - - Json::Value tmpDicomAsJson; - ReadDicomAsJson(tmpDicomAsJson, instanceId_, resource.missingRequestedTags_ /* ignoreTagLength */); // read all tags from DICOM and avoid cropping requested tags - tagsFromJson.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/); - } - else - { - tagsFromJson.FromDicomAsJson(*dicomAsJson, false /* append */, true /* parseSequences*/); - } - - resource.GetMainDicomTags().Merge(tagsFromJson); - } - - // compute the requested tags - ComputeTags(resource, *this, publicId, level, requestedTags); - } - else - { - return false; - } - - return true; - } - int64_t ServerContext::GetServerUpTime() const { boost::posix_time::ptime nowUtc = boost::posix_time::second_clock::universal_time(); @@ -2704,5 +1950,4 @@ return elapsed.total_seconds(); } - }
--- a/OrthancServer/Sources/ServerContext.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.h Thu Aug 29 15:41:42 2024 +0200 @@ -66,25 +66,6 @@ friend class ServerIndex; // To access "RemoveFile()" public: - class ILookupVisitor : public boost::noncopyable - { - public: - virtual ~ILookupVisitor() - { - } - - virtual bool IsDicomAsJsonNeeded() const = 0; - - virtual void MarkAsComplete() = 0; - - // NB: "dicomAsJson" must *not* be deleted, and can be NULL if - // "!IsDicomAsJsonNeeded()" - virtual void Visit(const std::string& publicId, - const std::string& instanceId, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson) = 0; - }; - struct StoreResult { private: @@ -445,21 +426,9 @@ void Stop(); - void Apply(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - size_t since, - size_t limit); - - void Apply(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - size_t since, - size_t limit) + uint64_t GetDatabaseLimits(ResourceType level) const { - Apply(visitor, lookup, queryLevel, std::set<std::string>(), LabelsConstraint_All, since, limit); + return (level == ResourceType_Instance ? limitFindInstances_ : limitFindResults_); } bool LookupOrReconstructMetadata(std::string& target, @@ -600,33 +569,6 @@ void SetUnknownSopClassAccepted(bool accepted); - bool ExpandResource(Json::Value& target, - const std::string& publicId, - ResourceType level, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess); - - bool ExpandResource(Json::Value& target, - const std::string& publicId, - const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) - const std::string& instanceId, // optional: the id of an instance for the resource - const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource - ResourceType level, - DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags, - bool allowStorageAccess); - - bool ExpandResource(ExpandedResource& target, - const std::string& publicId, - const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) - const std::string& instanceId, // optional: the id of an instance for the resource - const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource - ResourceType level, - const std::set<DicomTag>& requestedTags, - ExpandResourceFlags expandFlags, - bool allowStorageAccess); - FindStorageAccessMode GetFindStorageAccessMode() const { return findStorageAccessMode_;
--- a/OrthancServer/Sources/ServerIndex.h Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/ServerIndex.h Thu Aug 29 15:41:42 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); }; }
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Thu Aug 29 15:41:42 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>(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<std::string> childrenIds = targetPatient.childrenIds_; + const FindResponse::Resource& targetPatient = response.GetResourceByIndex(0); + + const std::set<std::string>& 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<DicomTag> mainPatientTags; DicomMap::GetMainDicomTags(mainPatientTags, ResourceType_Patient);
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Thu Aug 29 13:46:49 2024 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Thu Aug 29 15:41:42 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<std::string> 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<std::string> noLabel; transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);