Mercurial > hg > orthanc
changeset 5843:7df3d533c294 find-refactoring-clean
integration find-refactoring->find-refactoring-clean
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 29 Oct 2024 13:11:40 +0000 |
parents | 58c549b881ae (current diff) 08e47734328e (diff) |
children | f924d9a88cd2 0b56ea2fcdfc |
files | OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp |
diffstat | 14 files changed, 531 insertions(+), 292 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Wed Oct 09 11:01:11 2024 +0200 +++ b/NEWS Tue Oct 29 13:11:40 2024 +0000 @@ -38,9 +38,13 @@ - 'OrderBy' to order by DICOM Tag or metadata value - 'ParentPatient', 'ParentStudy', 'ParentSeries' to retrieve only descendants of an Orthanc resource. - - 'QueryMetadata' to filter results based on metadata values. + - 'MetadataQuery' to filter results based on metadata values. - 'ResponseContent' to define what shall be included in the response for each returned resource (e.g: Metadata, Children, ...) +* With DB backend with "HasExtendedFind" support, a new /tools/count-resources API route + is similar to tools/find but only returns the number of resources matching the criteria. +* With DB backend with "HasExtendedFind" support, usage of 'Limit' and 'Since in /tools/find + is not allowed if your query includes filtering on DICOM tags that are not stored in DB. Maintenance
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Oct 29 13:11:40 2024 +0000 @@ -1475,6 +1475,63 @@ } + virtual void ExecuteCount(uint64_t& count, + 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)); + } + + for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it) + { + Convert(*dbRequest.mutable_find()->add_metadata_constraints(), *(*it)); + } + + 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())); + + DatabasePluginMessages::TransactionResponse dbResponse; + ExecuteTransaction(dbResponse, DatabasePluginMessages::OPERATION_COUNT_RESOURCES, dbRequest); + + count = dbResponse.count_resources().count(); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + virtual void ExecuteFind(FindResponse& response, const FindRequest& request, const Capabilities& capabilities) ORTHANC_OVERRIDE
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Tue Oct 29 13:11:40 2024 +0000 @@ -314,6 +314,7 @@ OPERATION_UPDATE_AND_GET_STATISTICS = 49; // New in Orthanc 1.12.3 OPERATION_FIND = 50; // New in Orthanc 1.12.5 OPERATION_GET_CHANGES_EXTENDED = 51; // New in Orthanc 1.12.5 + OPERATION_COUNT_RESOURCES = 52; // New in Orthanc 1.12.5 } message Rollback { @@ -963,6 +964,14 @@ } } +message CountResources +{ + message Response + { + uint64 count = 1; + } +} + message TransactionRequest { sfixed64 transaction = 1; TransactionOperation operation = 2; @@ -1019,6 +1028,7 @@ UpdateAndGetStatistics.Request update_and_get_statistics = 149; Find.Request find = 150; GetChangesExtended.Request get_changes_extended = 151; + Find.Request count_resources = 152; } message TransactionResponse { @@ -1074,6 +1084,7 @@ UpdateAndGetStatistics.Response update_and_get_statistics = 149; repeated Find.Response find = 150; // One message per found resources GetChangesExtended.Response get_changes_extended = 151; + CountResources.Response count_resources = 152; } enum RequestType {
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Tue Oct 29 13:11:40 2024 +0000 @@ -58,6 +58,13 @@ + void BaseDatabaseWrapper::BaseTransaction::ExecuteCount(uint64_t& count, + const FindRequest& request, + const Capabilities& capabilities) + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(FindResponse& response, const FindRequest& request, const Capabilities& capabilities)
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Tue Oct 29 13:11:40 2024 +0000 @@ -48,6 +48,10 @@ int64_t& compressedSize, int64_t& uncompressedSize) ORTHANC_OVERRIDE; + virtual void ExecuteCount(uint64_t& count, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE; + virtual void ExecuteFind(FindResponse& response, const FindRequest& request, const Capabilities& capabilities) ORTHANC_OVERRIDE;
--- a/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Tue Oct 29 13:11:40 2024 +0000 @@ -123,6 +123,11 @@ throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels"); } + if (!request.GetOrdering().empty()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support ordering"); + } + if (IsRequestWithoutContraint(request) && !request.GetOrthancIdentifiers().HasPatientId() && !request.GetOrthancIdentifiers().HasStudyId() &&
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Oct 29 13:11:40 2024 +0000 @@ -380,10 +380,15 @@ int64_t& uncompressedSize) = 0; /** - * Primitives introduced in Orthanc 1.12.4 + * Primitives introduced in Orthanc 1.12.5 **/ // This is only implemented if "HasIntegratedFind()" is "true" + virtual void ExecuteCount(uint64_t& count, + const FindRequest& request, + const Capabilities& capabilities) = 0; + + // This is only implemented if "HasIntegratedFind()" is "true" virtual void ExecuteFind(FindResponse& response, const FindRequest& request, const Capabilities& capabilities) = 0;
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Oct 29 13:11:40 2024 +0000 @@ -502,6 +502,25 @@ #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) + virtual void ExecuteCount(uint64_t& count, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE + { + LookupFormatter formatter; + std::string sql; + + std::string lookupSql; + LookupFormatter::Apply(lookupSql, formatter, request); + + // base query, retrieve the ordered internalId and publicId of the selected resources + sql = "WITH Lookup AS (" + lookupSql + ") SELECT COUNT(*) FROM Lookup"; + SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql); + formatter.Bind(s); + + s.Step(); + count = s.ColumnInt64(0); + } + virtual void ExecuteFind(FindResponse& response, const FindRequest& request,
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Oct 29 13:11:40 2024 +0000 @@ -586,28 +586,25 @@ const std::string& publicId, ResourceType level) { - class Operations : public ReadOnlyOperationsT3<std::map<MetadataType, std::string>&, const std::string&, ResourceType> + FindRequest request(level); + request.SetOrthancId(level, publicId); + request.SetRetrieveMetadata(true); + + FindResponse response; + ExecuteFind(response, request); + + if (response.GetSize() == 0) { - public: - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - ResourceType type; - int64_t id; - if (!transaction.LookupResource(id, type, tuple.get<1>()) || - tuple.get<2>() != type) - { - throw OrthancException(ErrorCode_UnknownResource); - } - else - { - transaction.GetAllMetadata(tuple.get<0>(), id); - } - } - }; - - Operations operations; - operations.Apply(*this, target, publicId, level); + throw OrthancException(ErrorCode_UnknownResource); + } + else if (response.GetSize() == 1) + { + target = response.GetResourceByIndex(0).GetMetadata(level); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } } @@ -650,19 +647,17 @@ void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target, ResourceType resourceType) { - class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, ResourceType> + // This method is tested by "orthanc-tests/Plugins/WebDav/Run.py" + FindRequest request(resourceType); + + FindResponse response; + ExecuteFind(response, request); + + target.clear(); + for (size_t i = 0; i < response.GetSize(); i++) { - public: - virtual void ApplyTuple(ReadOnlyTransaction& transaction, - const Tuple& tuple) ORTHANC_OVERRIDE - { - // TODO - CANDIDATE FOR "TransactionType_Implicit" - transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>()); - } - }; - - Operations operations; - operations.Apply(*this, target, resourceType); + target.push_back(response.GetResourceByIndex(i).GetIdentifier()); + } } @@ -3377,6 +3372,33 @@ return db_.GetDatabaseCapabilities().HasFindSupport(); } + void StatelessDatabaseOperations::ExecuteCount(uint64_t& count, + const FindRequest& request) + { + class IntegratedCount : public ReadOnlyOperationsT3<uint64_t&, const FindRequest&, + const IDatabaseWrapper::Capabilities&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteCount(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + IDatabaseWrapper::Capabilities capabilities = db_.GetDatabaseCapabilities(); + + if (db_.HasIntegratedFind()) + { + IntegratedCount operations; + operations.Apply(*this, count, request, capabilities); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + void StatelessDatabaseOperations::ExecuteFind(FindResponse& response, const FindRequest& request) {
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Oct 29 13:11:40 2024 +0000 @@ -311,6 +311,13 @@ bool HasReachedMaxPatientCount(unsigned int maximumPatientCount, const std::string& patientId); + void ExecuteCount(uint64_t& count, + const FindRequest& request, + const IDatabaseWrapper::Capabilities& capabilities) + { + transaction_.ExecuteCount(count, request, capabilities); + } + void ExecuteFind(FindResponse& response, const FindRequest& request, const IDatabaseWrapper::Capabilities& capabilities) @@ -750,5 +757,9 @@ void ExecuteFind(FindResponse& response, const FindRequest& request); + + void ExecuteCount(uint64_t& count, + const FindRequest& request); + }; }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Oct 29 13:11:40 2024 +0000 @@ -3039,6 +3039,13 @@ } + enum FindType + { + FindType_Find, + FindType_Count + }; + + template <enum FindType requestType> static void Find(RestApiPostCall& call) { static const char* const KEY_CASE_SENSITIVE = "CaseSensitive"; @@ -3057,57 +3064,73 @@ static const char* const KEY_PARENT_PATIENT = "ParentPatient"; // New in Orthanc 1.12.5 static const char* const KEY_PARENT_STUDY = "ParentStudy"; // New in Orthanc 1.12.5 static const char* const KEY_PARENT_SERIES = "ParentSeries"; // New in Orthanc 1.12.5 - static const char* const KEY_QUERY_METADATA = "QueryMetadata"; // New in Orthanc 1.12.5 + static const char* const KEY_METADATA_QUERY = "MetadataQuery"; // New in Orthanc 1.12.5 static const char* const KEY_RESPONSE_CONTENT = "ResponseContent"; // New in Orthanc 1.12.5 if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human); - call.GetDocumentation() - .SetTag("System") - .SetSummary("Look for local resources") - .SetDescription("This URI can be used to perform a search on the content of the local Orthanc server, " - "in a way that is similar to querying remote DICOM modalities using C-FIND SCU: " - "https://orthanc.uclouvain.be/book/users/rest.html#performing-finds-within-orthanc") + RestApiCallDocumentation& doc = call.GetDocumentation(); + + doc.SetTag("System") .SetRequestField(KEY_CASE_SENSITIVE, RestApiCallDocumentation::Type_Boolean, "Enable case-sensitive search for PN value representations (defaults to configuration option `CaseSensitivePN`)", false) - .SetRequestField(KEY_EXPAND, RestApiCallDocumentation::Type_Boolean, - "Also retrieve the content of the matching resources, not only their Orthanc identifiers", false) .SetRequestField(KEY_LEVEL, RestApiCallDocumentation::Type_String, "Level of the query (`Patient`, `Study`, `Series` or `Instance`)", true) - .SetRequestField(KEY_LIMIT, RestApiCallDocumentation::Type_Number, - "Limit the number of reported resources", false) - .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number, - "Show only the resources since the provided index (in conjunction with `Limit`)", false) - .SetRequestField(KEY_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings, - "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true). " - "The tags requested tags are returned in the 'RequestedTags' field in the response. " - "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response " - "might be slow since Orthanc will need to access the DICOM files. If not specified, Orthanc will return " - "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false) .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject, "Associative array containing the filter on the values of the DICOM tags", true) .SetRequestField(KEY_LABELS, RestApiCallDocumentation::Type_JsonListOfStrings, "List of strings specifying which labels to look for in the resources (new in Orthanc 1.12.0)", true) .SetRequestField(KEY_LABELS_CONSTRAINT, RestApiCallDocumentation::Type_String, "Constraint on the labels, can be `All`, `Any`, or `None` (defaults to `All`, new in Orthanc 1.12.0)", true) - .SetRequestField(KEY_ORDER_BY, RestApiCallDocumentation::Type_JsonListOfObjects, - "Array of associative arrays containing the requested ordering (new in Orthanc 1.12.5)", true) .SetRequestField(KEY_PARENT_PATIENT, RestApiCallDocumentation::Type_String, "Limit the reported resources to descendants of this patient (new in Orthanc 1.12.5)", true) .SetRequestField(KEY_PARENT_STUDY, RestApiCallDocumentation::Type_String, "Limit the reported resources to descendants of this study (new in Orthanc 1.12.5)", true) .SetRequestField(KEY_PARENT_SERIES, RestApiCallDocumentation::Type_String, "Limit the reported resources to descendants of this series (new in Orthanc 1.12.5)", true) - .SetRequestField(KEY_QUERY_METADATA, RestApiCallDocumentation::Type_JsonObject, - "Associative array containing the filter on the values of the metadata (new in Orthanc 1.12.5)", true) - .SetRequestField(KEY_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings, - "Defines the content of response for each returned resource. Allowed values are `MainDicomTags`, " - "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`. " - "(new in Orthanc 1.12.5)", true) - .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information " - "about the reported resources (if `Expand` argument is `true`)"); + .SetRequestField(KEY_METADATA_QUERY, RestApiCallDocumentation::Type_JsonObject, + "Associative array containing the filter on the values of the metadata (new in Orthanc 1.12.5)", true); + + switch (requestType) + { + case FindType_Find: + doc.SetSummary("Look for local resources") + .SetDescription("This URI can be used to perform a search on the content of the local Orthanc server, " + "in a way that is similar to querying remote DICOM modalities using C-FIND SCU: " + "https://orthanc.uclouvain.be/book/users/rest.html#performing-finds-within-orthanc") + .SetRequestField(KEY_EXPAND, RestApiCallDocumentation::Type_Boolean, + "Also retrieve the content of the matching resources, not only their Orthanc identifiers", false) + .SetRequestField(KEY_LIMIT, RestApiCallDocumentation::Type_Number, + "Limit the number of reported resources", false) + .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number, + "Show only the resources since the provided index (in conjunction with `Limit`)", false) + .SetRequestField(KEY_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings, + "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true). " + "The tags requested tags are returned in the 'RequestedTags' field in the response. " + "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response " + "might be slow since Orthanc will need to access the DICOM files. If not specified, Orthanc will return " + "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false) + .SetRequestField(KEY_ORDER_BY, RestApiCallDocumentation::Type_JsonListOfObjects, + "Array of associative arrays containing the requested ordering (new in Orthanc 1.12.5)", true) + .SetRequestField(KEY_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings, + "Defines the content of response for each returned resource. Allowed values are `MainDicomTags`, " + "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`. " + "(new in Orthanc 1.12.5)", true) + .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information " + "about the reported resources (if `Expand` argument is `true`)"); + break; + case FindType_Count: + doc.SetSummary("Count local resources") + .SetDescription("This URI can be used to count the resources that are matching criterias on the content of the local Orthanc server, " + "in a way that is similar to tools/find") + .AddAnswerType(MimeType_Json, "A JSON object with the `Count` of matching resources"); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); + } + return; } @@ -3138,30 +3161,6 @@ throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" must be a Boolean"); } - else if (request.isMember(KEY_LIMIT) && - request[KEY_LIMIT].type() != Json::intValue) - { - throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_LIMIT) + "\" must be an integer"); - } - else if (request.isMember(KEY_SINCE) && - request[KEY_SINCE].type() != Json::intValue) - { - throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_SINCE) + "\" must be an integer"); - } - else if (request.isMember(KEY_REQUESTED_TAGS) && - request[KEY_REQUESTED_TAGS].type() != Json::arrayValue) - { - throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array"); - } - else if (request.isMember(KEY_RESPONSE_CONTENT) && - request[KEY_RESPONSE_CONTENT].type() != Json::arrayValue) - { - throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_RESPONSE_CONTENT) + "\" must be an array"); - } else if (request.isMember(KEY_LABELS) && request[KEY_LABELS].type() != Json::arrayValue) { @@ -3174,17 +3173,11 @@ throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings"); } - else if (request.isMember(KEY_ORDER_BY) && - request[KEY_ORDER_BY].type() != Json::arrayValue) + else if (request.isMember(KEY_METADATA_QUERY) && + request[KEY_METADATA_QUERY].type() != Json::objectValue) { throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_ORDER_BY) + "\" must be an array"); - } - else if (request.isMember(KEY_QUERY_METADATA) && - request[KEY_QUERY_METADATA].type() != Json::objectValue) - { - throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_QUERY_METADATA) + "\" must be an JSON object"); + "Field \"" + std::string(KEY_METADATA_QUERY) + "\" must be an JSON object"); } else if (request.isMember(KEY_PARENT_PATIENT) && request[KEY_PARENT_PATIENT].type() != Json::stringValue) @@ -3204,60 +3197,68 @@ throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_PARENT_SERIES) + "\" must be a string"); } + else if (requestType == FindType_Find && request.isMember(KEY_LIMIT) && + request[KEY_LIMIT].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LIMIT) + "\" must be an integer"); + } + else if (requestType == FindType_Find && request.isMember(KEY_SINCE) && + request[KEY_SINCE].type() != Json::intValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_SINCE) + "\" must be an integer"); + } + else if (requestType == FindType_Find && request.isMember(KEY_REQUESTED_TAGS) && + request[KEY_REQUESTED_TAGS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array"); + } + else if (requestType == FindType_Find && request.isMember(KEY_RESPONSE_CONTENT) && + request[KEY_RESPONSE_CONTENT].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_RESPONSE_CONTENT) + "\" must be an array"); + } + else if (requestType == FindType_Find && request.isMember(KEY_ORDER_BY) && + request[KEY_ORDER_BY].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_ORDER_BY) + "\" must be an array"); + } else if (true) { ResponseContentFlags responseContent = ResponseContentFlags_ID; - if (request.isMember(KEY_RESPONSE_CONTENT)) + if (requestType == FindType_Find) { - responseContent = ResponseContentFlags_Default; - - for (Json::ArrayIndex i = 0; i < request[KEY_RESPONSE_CONTENT].size(); ++i) + if (request.isMember(KEY_RESPONSE_CONTENT)) { - responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | StringToResponseContent(request[KEY_RESPONSE_CONTENT][i].asString())); + responseContent = ResponseContentFlags_Default; + + for (Json::ArrayIndex i = 0; i < request[KEY_RESPONSE_CONTENT].size(); ++i) + { + responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | StringToResponseContent(request[KEY_RESPONSE_CONTENT][i].asString())); + } + } + else if (request.isMember(KEY_EXPAND) && request[KEY_EXPAND].asBool()) + { + responseContent = ResponseContentFlags_ExpandTrue; } } - else if (request.isMember(KEY_EXPAND) && request[KEY_EXPAND].asBool()) + else if (requestType == FindType_Count) { - responseContent = ResponseContentFlags_ExpandTrue; + responseContent = ResponseContentFlags_INTERNAL_CountResources; } const ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); ResourceFinder finder(level, responseContent); - finder.SetDatabaseLimits(context.GetDatabaseLimits(level)); - - const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human); - - if (request.isMember(KEY_LIMIT)) - { - int64_t tmp = request[KEY_LIMIT].asInt64(); - if (tmp < 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer"); - } - else if (tmp != 0) // This is for compatibility with Orthanc 1.12.4 - { - finder.SetLimitsCount(static_cast<uint64_t>(tmp)); - } - } - - if (request.isMember(KEY_SINCE)) - { - int64_t tmp = request[KEY_SINCE].asInt64(); - if (tmp < 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer"); - } - else - { - finder.SetLimitsSince(static_cast<uint64_t>(tmp)); - } - } - - { + + DatabaseLookup dicomTagLookup; + + { // common query code bool caseSensitive = false; if (request.isMember(KEY_CASE_SENSITIVE)) { @@ -3265,7 +3266,6 @@ } { // DICOM Tag query - DatabaseLookup dicomTagLookup; Json::Value::Members members = request[KEY_QUERY].getMemberNames(); for (size_t i = 0; i < members.size(); i++) @@ -3288,21 +3288,27 @@ } } + if (requestType == FindType_Count && !dicomTagLookup.HasOnlyMainDicomTags()) + { + throw OrthancException(ErrorCode_BadRequest, + "Unable to count resources when querying tags that are not stored as MainDicomTags in the Database"); + } + finder.SetDatabaseLookup(dicomTagLookup); } { // Metadata query - Json::Value::Members members = request[KEY_QUERY_METADATA].getMemberNames(); + Json::Value::Members members = request[KEY_METADATA_QUERY].getMemberNames(); for (size_t i = 0; i < members.size(); i++) { - if (request[KEY_QUERY_METADATA][members[i]].type() != Json::stringValue) + if (request[KEY_METADATA_QUERY][members[i]].type() != Json::stringValue) { throw OrthancException(ErrorCode_BadRequest, "Tag \"" + members[i] + "\" must be associated with a string"); } MetadataType metadata = StringToMetadata(members[i]); - const std::string value = request[KEY_QUERY_METADATA][members[i]].asString(); + const std::string value = request[KEY_METADATA_QUERY][members[i]].asString(); if (!value.empty()) { @@ -3324,132 +3330,186 @@ } } } - } - - if (request.isMember(KEY_REQUESTED_TAGS)) - { - std::set<DicomTag> requestedTags; - FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]); - finder.AddRequestedTags(requestedTags); + + { // labels query + if (request.isMember(KEY_LABELS)) // New in Orthanc 1.12.0 + { + for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++) + { + if (request[KEY_LABELS][i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings"); + } + else + { + finder.AddLabel(request[KEY_LABELS][i].asString()); + } + } + } + + finder.SetLabelsConstraint(LabelsConstraint_All); + + if (request.isMember(KEY_LABELS_CONSTRAINT)) + { + const std::string& s = request[KEY_LABELS_CONSTRAINT].asString(); + if (s == "All") + { + finder.SetLabelsConstraint(LabelsConstraint_All); + } + else if (s == "Any") + { + finder.SetLabelsConstraint(LabelsConstraint_Any); + } + else if (s == "None") + { + finder.SetLabelsConstraint(LabelsConstraint_None); + } + else + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\""); + } + } + } + + // parents query + if (request.isMember(KEY_PARENT_PATIENT)) // New in Orthanc 1.12.5 + { + finder.SetOrthancId(ResourceType_Patient, request[KEY_PARENT_PATIENT].asString()); + } + else if (request.isMember(KEY_PARENT_STUDY)) + { + finder.SetOrthancId(ResourceType_Study, request[KEY_PARENT_STUDY].asString()); + } + else if (request.isMember(KEY_PARENT_SERIES)) + { + finder.SetOrthancId(ResourceType_Series, request[KEY_PARENT_SERIES].asString()); + } } - if (request.isMember(KEY_LABELS)) // New in Orthanc 1.12.0 + // response + if (requestType == FindType_Find) { - for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++) + const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human); + + finder.SetDatabaseLimits(context.GetDatabaseLimits(level)); + + if ((request.isMember(KEY_LIMIT) || request.isMember(KEY_SINCE)) && + !dicomTagLookup.HasOnlyMainDicomTags()) + { + throw OrthancException(ErrorCode_BadRequest, + "Unable to use " + std::string(KEY_LIMIT) + " or " + std::string(KEY_SINCE) + " in tools/find when querying tags that are not stored as MainDicomTags in the Database"); + } + + if (request.isMember(KEY_LIMIT)) { - if (request[KEY_LABELS][i].type() != Json::stringValue) + int64_t tmp = request[KEY_LIMIT].asInt64(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer"); + } + else if (tmp != 0) // This is for compatibility with Orthanc 1.12.4 { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings"); + finder.SetLimitsCount(static_cast<uint64_t>(tmp)); + } + } + + if (request.isMember(KEY_SINCE)) + { + int64_t tmp = request[KEY_SINCE].asInt64(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer"); } else { - finder.AddLabel(request[KEY_LABELS][i].asString()); + finder.SetLimitsSince(static_cast<uint64_t>(tmp)); } } - } - - finder.SetLabelsConstraint(LabelsConstraint_All); - - if (request.isMember(KEY_LABELS_CONSTRAINT)) - { - const std::string& s = request[KEY_LABELS_CONSTRAINT].asString(); - if (s == "All") + + if (request.isMember(KEY_REQUESTED_TAGS)) { - finder.SetLabelsConstraint(LabelsConstraint_All); - } - else if (s == "Any") - { - finder.SetLabelsConstraint(LabelsConstraint_Any); - } - else if (s == "None") - { - finder.SetLabelsConstraint(LabelsConstraint_None); - } - else - { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\""); + std::set<DicomTag> requestedTags; + FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]); + finder.AddRequestedTags(requestedTags); } - } - - if (request.isMember(KEY_PARENT_PATIENT)) // New in Orthanc 1.12.5 - { - finder.SetOrthancId(ResourceType_Patient, request[KEY_PARENT_PATIENT].asString()); - } - else if (request.isMember(KEY_PARENT_STUDY)) - { - finder.SetOrthancId(ResourceType_Study, request[KEY_PARENT_STUDY].asString()); - } - else if (request.isMember(KEY_PARENT_SERIES)) - { - finder.SetOrthancId(ResourceType_Series, request[KEY_PARENT_SERIES].asString()); - } - - if (request.isMember(KEY_ORDER_BY)) // New in Orthanc 1.12.5 - { - for (Json::Value::ArrayIndex i = 0; i < request[KEY_ORDER_BY].size(); i++) + + if (request.isMember(KEY_ORDER_BY)) // New in Orthanc 1.12.5 { - if (request[KEY_ORDER_BY][i].type() != Json::objectValue) + for (Json::Value::ArrayIndex i = 0; i < request[KEY_ORDER_BY].size(); i++) { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY) + "\" must contain objects"); - } - else - { - const Json::Value& order = request[KEY_ORDER_BY][i]; - FindRequest::OrderingDirection direction; - std::string directionString; - std::string typeString; - - if (!order.isMember(KEY_ORDER_BY_KEY) || order[KEY_ORDER_BY_KEY].type() != Json::stringValue) + if (request[KEY_ORDER_BY][i].type() != Json::objectValue) { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_KEY) + "\" must be a string"); - } - - if (!order.isMember(KEY_ORDER_BY_DIRECTION) || order[KEY_ORDER_BY_DIRECTION].type() != Json::stringValue) - { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\""); - } - - Toolbox::ToLowerCase(directionString, order[KEY_ORDER_BY_DIRECTION].asString()); - if (directionString == "asc") - { - direction = FindRequest::OrderingDirection_Ascending; - } - else if (directionString == "desc") - { - direction = FindRequest::OrderingDirection_Descending; + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY) + "\" must contain objects"); } else { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\""); - } - - if (!order.isMember(KEY_ORDER_BY_TYPE) || order[KEY_ORDER_BY_TYPE].type() != Json::stringValue) - { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\""); - } - - Toolbox::ToLowerCase(typeString, order[KEY_ORDER_BY_TYPE].asString()); - if (typeString == "dicomtag") - { - DicomTag tag = FromDcmtkBridge::ParseTag(order[KEY_ORDER_BY_KEY].asString()); - finder.AddOrdering(tag, direction); - } - else if (typeString == "metadata") - { - MetadataType metadata = StringToMetadata(order[KEY_ORDER_BY_KEY].asString()); - finder.AddOrdering(metadata, direction); - } - else - { - throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\""); + const Json::Value& order = request[KEY_ORDER_BY][i]; + FindRequest::OrderingDirection direction; + std::string directionString; + std::string typeString; + + if (!order.isMember(KEY_ORDER_BY_KEY) || order[KEY_ORDER_BY_KEY].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_KEY) + "\" must be a string"); + } + + if (!order.isMember(KEY_ORDER_BY_DIRECTION) || order[KEY_ORDER_BY_DIRECTION].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\""); + } + + Toolbox::ToLowerCase(directionString, order[KEY_ORDER_BY_DIRECTION].asString()); + if (directionString == "asc") + { + direction = FindRequest::OrderingDirection_Ascending; + } + else if (directionString == "desc") + { + direction = FindRequest::OrderingDirection_Descending; + } + else + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\""); + } + + if (!order.isMember(KEY_ORDER_BY_TYPE) || order[KEY_ORDER_BY_TYPE].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\""); + } + + Toolbox::ToLowerCase(typeString, order[KEY_ORDER_BY_TYPE].asString()); + if (typeString == "dicomtag") + { + DicomTag tag = FromDcmtkBridge::ParseTag(order[KEY_ORDER_BY_KEY].asString()); + finder.AddOrdering(tag, direction); + } + else if (typeString == "metadata") + { + MetadataType metadata = StringToMetadata(order[KEY_ORDER_BY_KEY].asString()); + finder.AddOrdering(metadata, direction); + } + else + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\""); + } } } } + + Json::Value answer; + finder.Execute(answer, context, format, false /* no "Metadata" field */); + call.GetOutput().AnswerJson(answer); } - - Json::Value answer; - finder.Execute(answer, context, format, false /* no "Metadata" field */); - call.GetOutput().AnswerJson(answer); + else if (requestType == FindType_Count) + { + uint64_t count = finder.Count(context); + Json::Value answer; + answer["Count"] = Json::Value::UInt64(count); + call.GetOutput().AnswerJson(answer); + } + } } @@ -4237,7 +4297,11 @@ } Register("/tools/lookup", Lookup); - Register("/tools/find", Find); + Register("/tools/find", Find<FindType_Find>); + if (context_.GetIndex().HasFindSupport()) + { + Register("/tools/count-resources", Find<FindType_Count>); + } Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>); Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>);
--- a/OrthancServer/Sources/ResourceFinder.cpp Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/ResourceFinder.cpp Tue Oct 29 13:11:40 2024 +0000 @@ -472,51 +472,73 @@ } - void ResourceFinder::UpdateRequestLimits() + void ResourceFinder::UpdateRequestLimits(ServerContext& context) { - // By default, use manual paging - pagingMode_ = PagingMode_FullManual; + if (context.GetIndex().HasFindSupport()) // in this case, limits are fully implemented in DB + { + pagingMode_ = PagingMode_FullDatabase; - if (databaseLimits_ != 0) - { - request_.SetLimits(0, databaseLimits_ + 1); + if (hasLimitsSince_ || hasLimitsCount_) + { + pagingMode_ = PagingMode_FullDatabase; + if (databaseLimits_ != 0 && limitsCount_ > databaseLimits_) + { + LOG(WARNING) << "ResourceFinder: 'Limit' is larger than LimitFindResults/LimitFindInstances configurations, using limit fron the configuration file"; + limitsCount_ = databaseLimits_; + } + + request_.SetLimits(limitsSince_, limitsCount_); + } + else if (databaseLimits_ != 0) + { + request_.SetLimits(0, databaseLimits_); + } + } else { - request_.ClearLimits(); - } - - if (lookup_.get() == NULL && - (hasLimitsSince_ || hasLimitsCount_)) - { - pagingMode_ = PagingMode_FullDatabase; - request_.SetLimits(limitsSince_, limitsCount_); - } + // By default, use manual paging + pagingMode_ = PagingMode_FullManual; - 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) + if (databaseLimits_ != 0) { - pagingMode_ = PagingMode_ManualSkip; - request_.SetLimits(0, limitsCount_ + limitsSince_); + request_.SetLimits(0, databaseLimits_ + 1); } else { + request_.ClearLimits(); + } + + if (lookup_.get() == NULL && + (hasLimitsSince_ || hasLimitsCount_)) + { pagingMode_ = PagingMode_FullDatabase; - request_.SetLimits(0, limitsCount_); + 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()" } @@ -543,8 +565,6 @@ isWarning005Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_005_RequestingTagFromLowerResourceLevel); } - UpdateRequestLimits(); - request_.SetRetrieveMainDicomTags(responseContent_ & ResponseContentFlags_MainDicomTags); request_.SetRetrieveMetadata((responseContent_ & ResponseContentFlags_Metadata) || (responseContent_ & ResponseContentFlags_MetadataLegacy)); request_.SetRetrieveLabels(responseContent_ & ResponseContentFlags_Labels); @@ -587,7 +607,6 @@ void ResourceFinder::SetDatabaseLimits(uint64_t limits) { databaseLimits_ = limits; - UpdateRequestLimits(); } @@ -601,7 +620,6 @@ { hasLimitsSince_ = true; limitsSince_ = since; - UpdateRequestLimits(); } } @@ -616,7 +634,6 @@ { hasLimitsCount_ = true; limitsCount_ = count; - UpdateRequestLimits(); } } @@ -674,8 +691,6 @@ throw OrthancException(ErrorCode_InternalError); } } - - UpdateRequestLimits(); } @@ -991,10 +1006,19 @@ } } + uint64_t ResourceFinder::Count(ServerContext& context) const + { + uint64_t count = 0; + context.GetIndex().ExecuteCount(count, request_); + return count; + } + void ResourceFinder::Execute(IVisitor& visitor, - ServerContext& context) const + ServerContext& context) { + UpdateRequestLimits(context); + bool isWarning002Enabled = false; bool isWarning004Enabled = false; @@ -1146,7 +1170,7 @@ void ResourceFinder::Execute(Json::Value& target, ServerContext& context, DicomToJsonFormat format, - bool includeAllMetadata) const + bool includeAllMetadata) { class Visitor : public IVisitor { @@ -1202,6 +1226,8 @@ } }; + UpdateRequestLimits(context); + target = Json::arrayValue; Visitor visitor(*this, context.GetIndex(), target, format, HasRequestedTags(), includeAllMetadata); @@ -1212,7 +1238,7 @@ bool ResourceFinder::ExecuteOneResource(Json::Value& target, ServerContext& context, DicomToJsonFormat format, - bool includeAllMetadata) const + bool includeAllMetadata) { Json::Value answer; Execute(answer, context, format, includeAllMetadata);
--- a/OrthancServer/Sources/ResourceFinder.h Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/ResourceFinder.h Tue Oct 29 13:11:40 2024 +0000 @@ -94,7 +94,7 @@ void InjectComputedTags(DicomMap& requestedTags, const FindResponse::Resource& resource) const; - void UpdateRequestLimits(); + void UpdateRequestLimits(ServerContext& context); bool HasRequestedTags() const { @@ -187,16 +187,18 @@ DicomToJsonFormat format) const; void Execute(IVisitor& visitor, - ServerContext& context) const; + ServerContext& context); void Execute(Json::Value& target, ServerContext& context, DicomToJsonFormat format, - bool includeAllMetadata) const; + bool includeAllMetadata); bool ExecuteOneResource(Json::Value& target, ServerContext& context, DicomToJsonFormat format, - bool includeAllMetadata) const; + bool includeAllMetadata); + + uint64_t Count(ServerContext& context) const; }; }
--- a/OrthancServer/Sources/ServerEnumerations.h Wed Oct 09 11:01:11 2024 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Tue Oct 29 13:11:40 2024 +0000 @@ -137,6 +137,8 @@ ResponseContentFlags_Labels = (1 << 11), ResponseContentFlags_IsStable = (1 << 12), + ResponseContentFlags_INTERNAL_CountResources = (1 << 31), + // Some predefined combinations ResponseContentFlags_ExpandTrue = (ResponseContentFlags_ID | ResponseContentFlags_Type |