# HG changeset patch # User Sebastien Jodogne # Date 1680621853 -7200 # Node ID 7363b6e7edf59ad7174cf1746a85c5dabf911882 # Parent 887df8c4583803b89ce56c26ef5cfb254d3e15e2# Parent bd25d1c333626c6f003e9fdffd3e9c9ec349620f integration mainline->db-protobuf diff -r bd25d1c33362 -r 7363b6e7edf5 NEWS --- a/NEWS Tue Apr 04 17:23:55 2023 +0200 +++ b/NEWS Tue Apr 04 17:24:13 2023 +0200 @@ -1,11 +1,20 @@ Pending changes in the mainline =============================== +General +------- + +* Support for labels associated with patients, studies, series, and instances + REST API -------- * API version upgraded to 20 -* /system: added "UserMetadata" +* New URIs "/.../{id}/labels/{label}" to test/set/remove labels +* "/tools/find" accepts the "WithLabels" and "WithoutLabels" arguments +* The "/patients/{id}", "/studies/{id}", "/series/{id}" and "/instances/{id}" + contain the "Labels" field +* "/system": added "UserMetadata" Plugins ------- @@ -18,7 +27,7 @@ * Enforce the existence of the patient/study/instance while creating its archive * Security: New configuration option "RestApiWriteToFileSystemEnabled" - to allow "/instances/../export" that is now disabled by default + to allow "/instances/../export" (the latter is now disabled by default) * Fix issue 214: VOILUTSequence is not returned in Wado-RS * Fix /tools/reset crashing when ExtraMainDicomTags were defined * Fix Housekeeper plugin infinite loop if Orthanc is empty. @@ -39,7 +48,7 @@ AcceptedTransferSyntaxes. * Made the default SQLite DB more robust wrt future updates like adding new columns in DB. -* Made the HTTP Client errors more verbose by including the url in the logs. +* Made the HTTP Client errors more verbose by including the URL in the logs. * Optimization: now using multiple threads to transcode files for asynchronous download of studies archive. * New configuration "KeepAliveTimeout" with a default value of 1 second. @@ -150,7 +159,7 @@ * Housekeeper plugin: Fix resume of previous processing * Added missing MOVEPatientRootQueryRetrieveInformationModel in DicomControlUserConnection::SetupPresentationContexts() -* Improved HttpClient error logging (add method + url) +* Improved HttpClient error logging (add method + URL) REST API -------- diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancFramework/UnitTestsSources/DicomMapTests.cpp --- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -716,7 +716,7 @@ DicomMap sequencesOnly; m.ExtractSequences(sequencesOnly); - ASSERT_EQ(1, sequencesOnly.GetSize()); + ASSERT_EQ(1u, sequencesOnly.GetSize()); ASSERT_TRUE(sequencesOnly.HasTag(0x0008, 0x1111)); ASSERT_TRUE(sequencesOnly.GetValue(0x0008, 0x1111).GetSequenceContent()[0].isMember("0008,1150")); @@ -724,7 +724,7 @@ DicomMap sequencesCopy; sequencesCopy.SetValue(0x0008, 0x1111, sequencesOnly.GetValue(0x0008, 0x1111)); - ASSERT_EQ(1, sequencesCopy.GetSize()); + ASSERT_EQ(1u, sequencesCopy.GetSize()); ASSERT_TRUE(sequencesCopy.HasTag(0x0008, 0x1111)); ASSERT_TRUE(sequencesCopy.GetValue(0x0008, 0x1111).GetSequenceContent()[0].isMember("0008,1150")); } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/CMakeLists.txt Tue Apr 04 17:24:13 2023 +0200 @@ -228,16 +228,15 @@ ##################################################################### set(ORTHANC_EMBEDDED_FILES - CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json - DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt - FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json - LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua - PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql - UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql - - INSTALL_TRACK_ATTACHMENTS_SIZE - ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql + CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua + PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql + INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql + INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql ) if (STANDALONE_BUILD) diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -565,8 +565,16 @@ std::list* instancesId, const std::vector& lookup, ResourceType queryLevel, + const std::set& withLabels, + const std::set& withoutLabels, uint32_t limit) ORTHANC_OVERRIDE { + if (!withLabels.empty() || + !withoutLabels.empty()) + { + throw OrthancException(ErrorCode_InternalError); // "HasLabelsSupport()" has returned "false" + } + if (that_.extensions_.lookupResources == NULL) { // Fallback to compatibility mode @@ -1413,6 +1421,27 @@ CheckSuccess(that_.extensions_.tagMostRecentPatient(that_.payload_, patient)); } } + + + virtual void AddLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + + virtual void RemoveLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + + virtual void ListLabels(std::set& target, + int64_t resource) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } }; diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Plugins/Engine/OrthancPluginDatabase.h --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Tue Apr 04 17:24:13 2023 +0200 @@ -113,6 +113,11 @@ return false; // No support for revisions in old API } + virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE + { + return false; + } + void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer); }; } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -800,8 +800,16 @@ std::list* instancesId, // Can be NULL if not needed const std::vector& lookup, ResourceType queryLevel, + const std::set& withLabels, + const std::set& withoutLabels, uint32_t limit) ORTHANC_OVERRIDE { + if (!withLabels.empty() || + !withoutLabels.empty()) + { + throw OrthancException(ErrorCode_InternalError); // "HasLabelsSupport()" has returned "false" + } + std::vector constraints; std::vector< std::vector > constraintsValues; @@ -1027,6 +1035,27 @@ return false; } } + + + virtual void AddLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + + virtual void RemoveLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + + virtual void ListLabels(std::set& target, + int64_t resource) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } }; diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Tue Apr 04 17:24:13 2023 +0200 @@ -82,6 +82,11 @@ IStorageArea& storageArea) ORTHANC_OVERRIDE; virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE; + + virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE + { + return false; + } }; } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -915,8 +915,17 @@ std::list* instancesId, // Can be NULL if not needed const std::vector& lookup, ResourceType queryLevel, + const std::set& withLabels, + const std::set& withoutLabels, uint32_t limit) ORTHANC_OVERRIDE { + if (!database_.HasLabelsSupport() && + (!withLabels.empty() || + !withoutLabels.empty())) + { + throw OrthancException(ErrorCode_InternalError); + } + DatabasePluginMessages::TransactionRequest request; request.mutable_lookup_resources()->set_query_level(Convert(queryLevel)); request.mutable_lookup_resources()->set_limit(limit); @@ -966,6 +975,12 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + if (!withLabels.empty() || + !withoutLabels.empty()) + { + throw OrthancException(ErrorCode_NotImplemented); // TODO + } DatabasePluginMessages::TransactionResponse response; ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES, request); @@ -1132,6 +1147,27 @@ return false; } } + + + virtual void AddLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + + virtual void RemoveLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + + virtual void ListLabels(std::set& target, + int64_t resource) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } }; @@ -1146,7 +1182,8 @@ open_(false), databaseVersion_(0), hasFlushToDisk_(false), - hasRevisionsSupport_(false) + hasRevisionsSupport_(false), + hasLabelsSupport_(false) { CLOG(INFO, PLUGINS) << "Identifier of this Orthanc server for the global properties " << "of the custom database: \"" << serverIdentifier << "\""; @@ -1186,6 +1223,7 @@ databaseVersion_ = response.get_system_information().database_version(); hasFlushToDisk_ = response.get_system_information().supports_flush_to_disk(); hasRevisionsSupport_ = response.get_system_information().supports_revisions(); + hasLabelsSupport_ = response.get_system_information().supports_labels(); } open_ = true; @@ -1307,4 +1345,17 @@ return hasRevisionsSupport_; } } + + + bool OrthancPluginDatabaseV4::HasLabelsSupport() const + { + if (!open_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return hasLabelsSupport_; + } + } } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Tue Apr 04 17:24:13 2023 +0200 @@ -44,6 +44,7 @@ unsigned int databaseVersion_; bool hasFlushToDisk_; bool hasRevisionsSupport_; + bool hasLabelsSupport_; void CheckSuccess(OrthancPluginErrorCode code) const; @@ -93,6 +94,8 @@ IStorageArea& storageArea) ORTHANC_OVERRIDE; virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE; + + virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE; }; } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto --- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Tue Apr 04 17:24:13 2023 +0200 @@ -129,6 +129,7 @@ uint32 database_version = 1; bool supports_flush_to_disk = 2; bool supports_revisions = 3; + bool supports_labels = 4; } } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Database/IDatabaseWrapper.h --- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Apr 04 17:24:13 2023 +0200 @@ -200,6 +200,8 @@ std::list* instancesId, // Can be NULL if not needed const std::vector& lookup, ResourceType queryLevel, + const std::set& withLabels, + const std::set& withoutLabels, uint32_t limit) = 0; // Returns "true" iff. the instance is new and has been inserted @@ -236,6 +238,20 @@ ResourceType& type, std::string& parentPublicId, const std::string& publicId) = 0; + + + /** + * Primitives introduced in Orthanc 1.12.0 + **/ + + virtual void AddLabel(int64_t resource, + const std::string& label) = 0; + + virtual void RemoveLabel(int64_t resource, + const std::string& label) = 0; + + virtual void ListLabels(std::set& target, + int64_t resource) = 0; }; @@ -260,5 +276,7 @@ IStorageArea& storageArea) = 0; virtual bool HasRevisionsSupport() const = 0; + + virtual bool HasLabelsSupport() const = 0; }; } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Database/InstallLabelsTable.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallLabelsTable.sql Tue Apr 04 17:24:13 2023 +0200 @@ -0,0 +1,28 @@ +-- 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) 2021-2023 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 . + + +CREATE TABLE Labels( + internalId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + label TEXT NOT NULL, + PRIMARY KEY(internalId, label) -- Prevents duplicates + ); + +CREATE INDEX LabelsIndex1 ON Labels(internalId); +CREATE INDEX LabelsIndex2 ON Labels(label); -- This index allows efficient lookups diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Database/PrepareDatabase.sql --- a/OrthancServer/Sources/Database/PrepareDatabase.sql Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Tue Apr 04 17:24:13 2023 +0200 @@ -91,6 +91,13 @@ patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE ); +-- New in Orthanc 1.12.0 +CREATE TABLE Labels( + internalId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + label TEXT NOT NULL, + PRIMARY KEY(internalId, label) -- Prevents duplicates + ); + CREATE INDEX ChildrenIndex ON Resources(parentId); CREATE INDEX PublicIndex ON Resources(publicId); CREATE INDEX ResourceTypeIndex ON Resources(resourceType); @@ -108,6 +115,10 @@ CREATE INDEX ChangesIndex ON Changes(internalId); +-- New in Orthanc 1.12.0 +CREATE INDEX LabelsIndex1 ON Labels(internalId); +CREATE INDEX LabelsIndex2 ON Labels(label); -- This index allows efficient lookups + CREATE TRIGGER AttachedFileDeleted AFTER DELETE ON AttachedFiles BEGIN diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -341,12 +341,14 @@ std::list* instancesId, const std::vector& lookup, ResourceType queryLevel, + const std::set& withLabels, + const std::set& withoutLabels, uint32_t limit) ORTHANC_OVERRIDE { LookupFormatter formatter; std::string sql; - LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit); + LookupFormatter::Apply(sql, formatter, lookup, queryLevel, withLabels, withoutLabels, limit); sql = "CREATE TEMPORARY TABLE Lookup AS " + sql; @@ -1071,6 +1073,56 @@ s.Run(); } } + + + virtual void AddLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + if (label.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR IGNORE INTO Labels (internalId, label) VALUES(?, ?)"); + s.BindInt64(0, resource); + s.BindString(1, label); + s.Run(); + } + } + + + virtual void RemoveLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + if (label.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Labels WHERE internalId=? AND label=?"); + s.BindInt64(0, resource); + s.BindString(1, label); + s.Run(); + } + } + + + virtual void ListLabels(std::set& target, + int64_t resource) ORTHANC_OVERRIDE + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT label FROM Labels WHERE internalId=?"); + s.BindInt64(0, resource); + + while (s.Step()) + { + target.insert(s.ColumnString(0)); + } + } }; @@ -1344,9 +1396,9 @@ "Incompatible version of the Orthanc database: " + tmp); } - // New in Orthanc 1.5.1 if (version_ == 6) { + // New in Orthanc 1.5.1 if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true /* unused in SQLite */) || tmp != "1") { @@ -1355,6 +1407,15 @@ ServerResources::GetFileResource(query, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); db_.Execute(query); } + + // New in Orthanc 1.12.0 + if (!db_.DoesTableExist("Labels")) + { + LOG(INFO) << "Installing the \"Labels\" table"; + std::string query; + ServerResources::GetFileResource(query, ServerResources::INSTALL_LABELS_TABLE); + db_.Execute(query); + } } transaction->Commit(0); diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Tue Apr 04 17:24:13 2023 +0200 @@ -97,6 +97,11 @@ return false; // TODO - REVISIONS } + virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE + { + return true; + } + /** * The "StartTransaction()" method is guaranteed to return a class diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -715,10 +715,10 @@ const std::string& publicId, ResourceType level, const std::set& requestedTags, - ExpandResourceDbFlags expandFlags) + ExpandResourceFlags expandFlags) { class Operations : public ReadOnlyOperationsT6< - bool&, ExpandedResource&, const std::string&, ResourceType, const std::set&, ExpandResourceDbFlags> + bool&, ExpandedResource&, const std::string&, ResourceType, const std::set&, ExpandResourceFlags> { private: @@ -778,7 +778,7 @@ else { ExpandedResource& target = tuple.get<1>(); - ExpandResourceDbFlags expandFlags = tuple.get<5>(); + ExpandResourceFlags expandFlags = tuple.get<5>(); // Set information about the parent resource (if it exists) if (type == ResourceType_Patient) @@ -798,16 +798,15 @@ target.parentId_ = parent; } - target.type_ = type; - target.id_ = tuple.get<2>(); - - if (expandFlags & ExpandResourceDbFlags_IncludeChildren) + target.SetResource(type, tuple.get<2>()); + + if (expandFlags & ExpandResourceFlags_IncludeChildren) { // List the children resources transaction.GetChildrenPublicId(target.childrenIds_, internalId); } - if (expandFlags & ExpandResourceDbFlags_IncludeMetadata) + if (expandFlags & ExpandResourceFlags_IncludeMetadata) { // Extract the metadata transaction.GetAllMetadata(target.metadata_, internalId); @@ -869,10 +868,10 @@ LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature); } - if (expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags) + if (expandFlags & ExpandResourceFlags_IncludeMainDicomTags) { // read all tags from DB - transaction.GetMainDicomTags(target.tags_, internalId); + transaction.GetMainDicomTags(target.GetMainDicomTags(), internalId); // read all main sequences from DB std::string serializedSequences; @@ -882,7 +881,7 @@ Toolbox::ReadJson(jsonMetadata, serializedSequences); assert(jsonMetadata["Version"].asInt() == 1); - target.tags_.FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */); + 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 @@ -895,7 +894,7 @@ FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, target.mainDicomTagsSignature_); // read parent main dicom tags as long as we have not gathered all requested tags - ResourceType currentLevel = target.type_; + ResourceType currentLevel = target.GetLevel(); int64_t currentInternalId = internalId; Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags); @@ -931,7 +930,7 @@ DicomMap parentTags; transaction.GetMainDicomTags(parentTags, currentParentId); - target.tags_.Merge(parentTags); + target.GetMainDicomTags().Merge(parentTags); } currentInternalId = currentParentId; @@ -939,6 +938,11 @@ } } + if (expandFlags & ExpandResourceFlags_IncludeLabels) + { + transaction.ListLabels(target.labels_, internalId); + } + std::string tmp; if (LookupStringMetadata(tmp, target.metadata_, MetadataType_AnonymizedFrom)) @@ -1066,7 +1070,7 @@ void StatelessDatabaseOperations::GetAllUuids(std::list& target, ResourceType resourceType, size_t since, - size_t limit) + uint32_t limit) { if (limit == 0) { @@ -1646,7 +1650,9 @@ { // TODO - CANDIDATE FOR "TransactionType_Implicit" std::list tmp; - transaction.ApplyLookupResources(tmp, NULL, query_, level_, 0); + std::set withLabels; + std::set withoutLabels; + transaction.ApplyLookupResources(tmp, NULL, query_, level_, withLabels, withoutLabels, 0); CopyListToVector(result_, tmp); } }; @@ -1915,9 +1921,12 @@ std::vector* instancesId, const DatabaseLookup& lookup, ResourceType queryLevel, - size_t limit) + const std::set& withLabels, + const std::set& withoutLabels, + uint32_t limit) { - class Operations : public ReadOnlyOperationsT4&, ResourceType, size_t> + class Operations : public ReadOnlyOperationsT6&, ResourceType, + const std::set&, const std::set&, size_t> { private: std::list resourcesList_; @@ -1940,11 +1949,13 @@ // TODO - CANDIDATE FOR "TransactionType_Implicit" if (tuple.get<0>()) { - transaction.ApplyLookupResources(resourcesList_, &instancesList_, tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); + 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>()); + transaction.ApplyLookupResources( + resourcesList_, NULL, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>()); } } }; @@ -1954,7 +1965,7 @@ NormalizeLookup(normalized, lookup, queryLevel); Operations operations; - operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, limit); + operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, withLabels, withoutLabels, limit); CopyListToVector(resourcesId, operations.GetResourcesList()); @@ -3526,4 +3537,96 @@ Apply(operations); return operations.GetStatus(); } + + + void StatelessDatabaseOperations::ListLabels(std::set& target, + const std::string& publicId, + ResourceType level) + { + class Operations : public ReadOnlyOperationsT3&, const std::string&, ResourceType> + { + 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.ListLabels(tuple.get<0>(), id); + } + } + }; + + Operations operations; + operations.Apply(*this, target, publicId, level); + } + + + void StatelessDatabaseOperations::ModifyLabel(const std::string& publicId, + ResourceType level, + const std::string& label, + LabelOperation operation) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& publicId_; + ResourceType level_; + const std::string& label_; + LabelOperation operation_; + + public: + Operations(const std::string& publicId, + ResourceType level, + const std::string& label, + LabelOperation operation) : + publicId_(publicId), + level_(level), + label_(label), + operation_(operation) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + ResourceType type; + int64_t id; + if (!transaction.LookupResource(id, type, publicId_) || + level_ != type) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + switch (operation_) + { + case LabelOperation_Add: + transaction.AddLabel(id, label_); + break; + + case LabelOperation_Remove: + transaction.RemoveLabel(id, label_); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } + }; + + if (!Toolbox::IsAsciiString(label)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "A label must only contain ASCII characters"); + } + + Operations operations(publicId, level, label, operation); + Apply(operations); + } } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Database/StatelessDatabaseOperations.h --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Apr 04 17:24:13 2023 +0200 @@ -37,15 +37,18 @@ class ParsedDicomFile; struct ServerIndexChange; - struct ExpandedResource : public boost::noncopyable + class ExpandedResource : public boost::noncopyable { + private: std::string id_; - DicomMap tags_; // all main tags and main sequences from DB + ResourceType level_; + DicomMap tags_; // all main tags and main sequences from DB + + public: std::string mainDicomTagsSignature_; std::string parentId_; std::list childrenIds_; std::map metadata_; - ResourceType type_; std::string anonymizedFrom_; std::string modifiedFrom_; std::string lastUpdate_; @@ -62,18 +65,51 @@ size_t fileSize_; std::string fileUuid_; int indexInSeries_; + + // New in Orthanc 1.12.0 + std::set labels_; + + public: + 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 ExpandResourceDbFlags + enum ExpandResourceFlags { - ExpandResourceDbFlags_None = 0, - ExpandResourceDbFlags_IncludeMetadata = (1 << 0), - ExpandResourceDbFlags_IncludeChildren = (1 << 1), - ExpandResourceDbFlags_IncludeMainDicomTags = (1 << 2), + ExpandResourceFlags_None = 0, + ExpandResourceFlags_IncludeMetadata = (1 << 0), + ExpandResourceFlags_IncludeChildren = (1 << 1), + ExpandResourceFlags_IncludeMainDicomTags = (1 << 2), + ExpandResourceFlags_IncludeLabels = (1 << 3), - ExpandResourceDbFlags_Default = (ExpandResourceDbFlags_IncludeMetadata | - ExpandResourceDbFlags_IncludeChildren | - ExpandResourceDbFlags_IncludeMainDicomTags) + ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata | + ExpandResourceFlags_IncludeChildren | + ExpandResourceFlags_IncludeMainDicomTags | + ExpandResourceFlags_IncludeLabels) }; class StatelessDatabaseOperations : public boost::noncopyable @@ -82,6 +118,12 @@ typedef std::list Attachments; typedef std::map, std::string> MetadataMap; + enum LabelOperation + { + LabelOperation_Add, + LabelOperation_Remove + }; + class ITransactionContext : public IDatabaseListener { public: @@ -157,9 +199,11 @@ std::list* instancesId, // Can be NULL if not needed const std::vector& lookup, ResourceType queryLevel, - size_t limit) + const std::set& withLabels, + const std::set& withoutLabels, + uint32_t limit) { - return transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit); + return transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, withLabels, withoutLabels, limit); } void GetAllMetadata(std::map& target, @@ -177,7 +221,7 @@ void GetAllPublicIds(std::list& target, ResourceType resourceType, size_t since, - size_t limit) + uint32_t limit) { return transaction_.GetAllPublicIds(target, resourceType, since, limit); } @@ -185,9 +229,9 @@ void GetChanges(std::list& target /*out*/, bool& done /*out*/, int64_t since, - uint32_t maxResults) + uint32_t limit) { - transaction_.GetChanges(target, done, since, maxResults); + transaction_.GetChanges(target, done, since, limit); } void GetChildrenInternalId(std::list& target, @@ -205,9 +249,9 @@ void GetExportedResources(std::list& target /*out*/, bool& done /*out*/, int64_t since, - uint32_t maxResults) + uint32_t limit) { - return transaction_.GetExportedResources(target, done, since, maxResults); + return transaction_.GetExportedResources(target, done, since, limit); } void GetLastChange(std::list& target /*out*/) @@ -310,6 +354,12 @@ { return transaction_.LookupResourceAndParent(id, type, parentPublicId, publicId); } + + void ListLabels(std::set& target, + int64_t id) + { + transaction_.ListLabels(target, id); + } }; @@ -422,6 +472,18 @@ unsigned int maximumPatients, uint64_t addedInstanceSize, const std::string& newPatientId); + + void AddLabel(int64_t id, + const std::string& label) + { + transaction_.AddLabel(id, label); + } + + void RemoveLabel(int64_t id, + const std::string& label) + { + transaction_.RemoveLabel(id, label); + } }; @@ -503,7 +565,7 @@ const std::string& publicId, ResourceType level, const std::set& requestedTags, - ExpandResourceDbFlags expandFlags); + ExpandResourceFlags expandFlags); void GetAllMetadata(std::map& target, const std::string& publicId, @@ -515,7 +577,7 @@ void GetAllUuids(std::list& target, ResourceType resourceType, size_t since, - size_t limit); + uint32_t limit); void GetGlobalStatistics(/* out */ uint64_t& diskSize, /* out */ uint64_t& uncompressedSize, @@ -531,13 +593,13 @@ void GetChanges(Json::Value& target, int64_t since, - unsigned int maxResults); + uint32_t limit); void GetLastChange(Json::Value& target); void GetExportedResources(Json::Value& target, int64_t since, - unsigned int maxResults); + uint32_t limit); void GetLastExportedResource(Json::Value& target); @@ -605,7 +667,9 @@ std::vector* instancesId, // Can be NULL if not needed const DatabaseLookup& lookup, ResourceType queryLevel, - size_t limit); + const std::set& withLabels, + const std::set& withoutLabels, + uint32_t limit); bool DeleteResource(Json::Value& remainingAncestor /* out */, const std::string& uuid, @@ -683,5 +747,14 @@ bool hasOldRevision, int64_t oldRevision, const std::string& oldMd5); + + void ListLabels(std::set& target, + const std::string& publicId, + ResourceType level); + + void ModifyLabel(const std::string& publicId, + ResourceType level, + const std::string& label, + LabelOperation operation); }; } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/OrthancFindRequestHandler.cpp --- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -59,7 +59,8 @@ 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, ExpandResourceDbFlags_IncludeMainDicomTags, allowStorageAccess); + context.ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, + level, requestedTags, ExpandResourceFlags_IncludeMainDicomTags, allowStorageAccess); DicomMap result; @@ -84,7 +85,7 @@ else { const DicomTag& tag = query.GetElement(i).GetTag(); - const DicomValue* value = resource.tags_.TestAndGetValue(tag); + const DicomValue* value = resource.GetMainDicomTags().TestAndGetValue(tag); if (value != NULL && !value->IsNull() && diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -1971,6 +1971,129 @@ + // Handling of labels ------------------------------------------------------- + + static void ListLabels(RestApiGetCall& call) + { + if (call.IsDocumentation()) + { + ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); + std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetSummary("List labels (new in Orthanc 1.12.0)") + .SetDescription("Get the labels that are associated with the given " + r) + .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") + .AddAnswerType(MimeType_Json, "JSON array containing the names of the labels") + .SetHttpGetSample(GetDocumentationSampleResource(t) + "/labels", true); + return; + } + + assert(!call.GetFullUri().empty()); + const std::string publicId = call.GetUriComponent("id", ""); + ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); + + std::set labels; + OrthancRestApi::GetIndex(call).ListLabels(labels, publicId, level); + + Json::Value result = Json::arrayValue; + + for (std::set::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + result.append(*it); + } + + call.GetOutput().AnswerJson(result); + } + + + static void GetLabel(RestApiGetCall& call) + { + if (call.IsDocumentation()) + { + ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); + std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetSummary("Test label") + .SetDescription("Test whether the " + r + " is associated with the given label") + .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") + .SetUriArgument("label", "The label of interest") + .AddAnswerType(MimeType_PlainText, "Empty string is returned in the case of presence, error 404 in the case of absence"); + return; + } + + CheckValidResourceType(call); + + assert(!call.GetFullUri().empty()); + const std::string publicId = call.GetUriComponent("id", ""); + const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); + + std::string label = call.GetUriComponent("label", ""); + + std::set labels; + OrthancRestApi::GetIndex(call).ListLabels(labels, publicId, level); + + if (labels.find(label) != labels.end()) + { + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + } + + + static void AddLabel(RestApiPutCall& call) + { + if (call.IsDocumentation()) + { + ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); + std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetSummary("Add label") + .SetDescription("Associate a label with a " + r) + .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") + .SetUriArgument("label", "The label to be added"); + return; + } + + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); + + std::string label = call.GetUriComponent("label", ""); + OrthancRestApi::GetIndex(call).ModifyLabel(publicId, level, label, StatelessDatabaseOperations::LabelOperation_Add); + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + + + static void RemoveLabel(RestApiDeleteCall& call) + { + if (call.IsDocumentation()) + { + ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); + std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetSummary("Remove label") + .SetDescription("Remove a label associated with a " + r) + .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") + .SetUriArgument("label", "The label to be removed"); + return; + } + + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); + + std::string label = call.GetUriComponent("label", ""); + OrthancRestApi::GetIndex(call).ModifyLabel(publicId, level, label, StatelessDatabaseOperations::LabelOperation_Remove); + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + // Handling of attached files ----------------------------------------------- @@ -2949,6 +3072,8 @@ static const char* const KEY_QUERY = "Query"; static const char* const KEY_REQUESTED_TAGS = "RequestedTags"; static const char* const KEY_SINCE = "Since"; + static const char* const KEY_WITH_LABELS = "WithLabels"; // New in Orthanc 1.12.0 + static const char* const KEY_WITHOUT_LABELS = "WithoutLabels"; // New in Orthanc 1.12.0 if (call.IsDocumentation()) { @@ -2978,6 +3103,10 @@ "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_WITH_LABELS, RestApiCallDocumentation::Type_JsonListOfStrings, + "List of strings specifying which labels must be present in the resources (new in Orthanc 1.12.0)", true) + .SetRequestField(KEY_WITHOUT_LABELS, RestApiCallDocumentation::Type_JsonListOfStrings, + "List of strings specifying which labels must not be present in the resources (new in Orthanc 1.12.0)", true) .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information " "about the reported resources (if `Expand` argument is `true`)"); return; @@ -3008,25 +3137,37 @@ request[KEY_CASE_SENSITIVE].type() != Json::booleanValue) { throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean"); + "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) + "\" should be an integer"); + "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) + "\" should be an integer"); + "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) + "\" should be an array"); + "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array"); + } + else if (request.isMember(KEY_WITH_LABELS) && + request[KEY_WITH_LABELS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_WITH_LABELS) + "\" must be an array of strings"); + } + else if (request.isMember(KEY_WITHOUT_LABELS) && + request[KEY_WITHOUT_LABELS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_WITHOUT_LABELS) + "\" must be an array of strings"); } else { @@ -3049,7 +3190,7 @@ if (tmp < 0) { throw OrthancException(ErrorCode_ParameterOutOfRange, - "Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer"); + "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer"); } limit = static_cast(tmp); @@ -3062,7 +3203,7 @@ if (tmp < 0) { throw OrthancException(ErrorCode_ParameterOutOfRange, - "Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer"); + "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer"); } since = static_cast(tmp); @@ -3085,7 +3226,7 @@ if (request[KEY_QUERY][members[i]].type() != Json::stringValue) { throw OrthancException(ErrorCode_BadRequest, - "Tag \"" + members[i] + "\" should be associated with a string"); + "Tag \"" + members[i] + "\" must be associated with a string"); } const std::string value = request[KEY_QUERY][members[i]].asString(); @@ -3100,6 +3241,36 @@ } } + if (request.isMember(KEY_WITH_LABELS)) // New in Orthanc 1.12.0 + { + for (Json::Value::ArrayIndex i = 0; i < request[KEY_WITH_LABELS].size(); i++) + { + if (request[KEY_WITH_LABELS][i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \""+ std::string(KEY_WITH_LABELS) + "\" must contain strings"); + } + else + { + query.AddWithLabel(request[KEY_WITH_LABELS][i].asString()); + } + } + } + + if (request.isMember(KEY_WITHOUT_LABELS)) // New in Orthanc 1.12.0 + { + for (Json::Value::ArrayIndex i = 0; i < request[KEY_WITHOUT_LABELS].size(); i++) + { + if (request[KEY_WITHOUT_LABELS][i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \""+ std::string(KEY_WITHOUT_LABELS) + "\" must contain strings"); + } + else + { + query.AddWithoutLabel(request[KEY_WITHOUT_LABELS][i].asString()); + } + } + } + FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human), context.GetFindStorageAccessMode()); context.Apply(visitor, query, level, since, limit); visitor.Answer(call.GetOutput(), context, level, expand, requestedTags); @@ -3854,6 +4025,12 @@ Register("/" + resourceTypes[i] + "/{id}/metadata/{name}", GetMetadata); Register("/" + resourceTypes[i] + "/{id}/metadata/{name}", SetMetadata); + // New in Orthanc 1.12.0 + Register("/" + resourceTypes[i] + "/{id}/labels", ListLabels); + Register("/" + resourceTypes[i] + "/{id}/labels/{label}", GetLabel); + Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel); + Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel); + Register("/" + resourceTypes[i] + "/{id}/attachments", ListAttachments); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", GetAttachmentOperations); diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Search/DatabaseLookup.cpp --- a/OrthancServer/Sources/Search/DatabaseLookup.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -368,4 +368,30 @@ return clone.release(); } + + + void DatabaseLookup::AddWithLabel(const std::string& label) + { + if (label.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + withLabels_.insert(label); + } + } + + + void DatabaseLookup::AddWithoutLabel(const std::string& label) + { + if (label.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + withoutLabels_.insert(label); + } + } } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Search/DatabaseLookup.h --- a/OrthancServer/Sources/Search/DatabaseLookup.h Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.h Tue Apr 04 17:24:13 2023 +0200 @@ -32,6 +32,8 @@ { private: std::vector constraints_; + std::set withLabels_; + std::set withoutLabels_; void AddDicomConstraintInternal(const DicomTag& tag, ValueRepresentation vr, @@ -92,5 +94,19 @@ bool HasTag(const DicomTag& tag) const; void RemoveConstraint(const DicomTag& tag); + + void AddWithLabel(const std::string& label); + + void AddWithoutLabel(const std::string& label); + + const std::set& GetWithLabels() const + { + return withLabels_; + } + + const std::set& GetWithoutLabels() const + { + return withoutLabels_; + } }; } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Search/ISqlLookupFormatter.cpp --- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -39,6 +39,7 @@ #include "DatabaseConstraint.h" #include +#include namespace Orthanc @@ -268,12 +269,46 @@ " AND " + tag + ".tagElement = " + boost::lexical_cast(constraint.GetTag().GetElement())); } + + + static std::string Join(const std::list& values, + const std::string& prefix, + const std::string& separator) + { + if (values.empty()) + { + return ""; + } + else + { + std::string s = prefix; + + bool first = true; + for (std::list::const_iterator it = values.begin(); it != values.end(); ++it) + { + if (first) + { + first = false; + } + else + { + s += separator; + } + + s += *it; + } + + return s; + } + } void ISqlLookupFormatter::Apply(std::string& sql, ISqlLookupFormatter& formatter, const std::vector& lookup, ResourceType queryLevel, + const std::set& withLabels, + const std::set& withoutLabels, size_t limit) { assert(ResourceType_Patient < ResourceType_Study && @@ -346,9 +381,44 @@ FormatLevel(static_cast(level - 1)) + ".internalId=" + FormatLevel(static_cast(level)) + ".parentId"); } - - sql += (joins + " WHERE " + FormatLevel(queryLevel) + ".resourceType = " + - formatter.FormatResourceType(queryLevel) + comparisons); + + std::list where; + + if (!withLabels.empty()) + { + std::list labels; + for (std::set::const_iterator it = withLabels.begin(); it != withLabels.end(); ++it) + { + labels.push_back(formatter.GenerateParameter(*it)); + } + + where.push_back(boost::lexical_cast(withLabels.size()) + + " = (SELECT COUNT(1) FROM Labels WHERE internalId = " + FormatLevel(queryLevel) + + ".internalId AND label IN (" + Join(labels, "", ", ") + "))"); + } + + if (!withoutLabels.empty()) + { + /** + * "In SQL Server, NOT EXISTS and NOT IN predicates are the best + * way to search for missing values, as long as both columns in + * question are NOT NULL." + * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/ + **/ + std::list labels; + for (std::set::const_iterator it = withoutLabels.begin(); it != withoutLabels.end(); ++it) + { + labels.push_back(formatter.GenerateParameter(*it)); + } + + where.push_back("NOT EXISTS (SELECT 1 FROM Labels WHERE internalId = " + FormatLevel(queryLevel) + + ".internalId AND label IN (" + Join(labels, "", ", ") + "))"); + } + + where.push_back(FormatLevel(queryLevel) + ".resourceType = " + + formatter.FormatResourceType(queryLevel) + comparisons); + + sql += joins + Join(where, " WHERE ", " AND "); if (limit != 0) { diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/Search/ISqlLookupFormatter.h --- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h Tue Apr 04 17:24:13 2023 +0200 @@ -60,6 +60,8 @@ ISqlLookupFormatter& formatter, const std::vector& lookup, ResourceType queryLevel, + const std::set& withLabels, // New in Orthanc 1.12.0 + const std::set& withoutLabels, // New in Orthanc 1.12.0 size_t limit); }; } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -1448,8 +1448,9 @@ } { - const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); - GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, lookupLimit); + const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); + GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, + lookup.GetWithLabels(), lookup.GetWithoutLabels(), lookupLimit); } bool complete = (databaseLimit == 0 || @@ -1542,7 +1543,7 @@ ComputeStudyTags(resource, *this, resources[i], requestedTags); std::vector modalities; - Toolbox::TokenizeString(modalities, resource.tags_.GetValue(DICOM_TAG_MODALITIES_IN_STUDY).GetContent(), '\\'); + Toolbox::TokenizeString(modalities, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY).GetContent(), '\\'); bool hasAtLeastOneModalityMatching = false; for (size_t m = 0; m < modalities.size(); m++) { @@ -1551,7 +1552,7 @@ 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.tags_.GetValue(DICOM_TAG_MODALITIES_IN_STUDY)); + allMainDicomTagsFromDB.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, resource.GetMainDicomTags().GetValue(DICOM_TAG_MODALITIES_IN_STUDY)); } if (isMatch) @@ -1990,10 +1991,10 @@ { target = Json::objectValue; - target["Type"] = GetResourceTypeText(resource.type_, false, true); - target["ID"] = resource.id_; + target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true); + target["ID"] = resource.GetPublicId(); - switch (resource.type_) + switch (resource.GetLevel()) { case ResourceType_Patient: break; @@ -2014,7 +2015,7 @@ throw OrthancException(ErrorCode_InternalError); } - switch (resource.type_) + switch (resource.GetLevel()) { case ResourceType_Patient: case ResourceType_Study: @@ -2028,11 +2029,11 @@ c.append(*it); } - if (resource.type_ == ResourceType_Patient) + if (resource.GetLevel() == ResourceType_Patient) { target["Studies"] = c; } - else if (resource.type_ == ResourceType_Study) + else if (resource.GetLevel() == ResourceType_Study) { target["Series"] = c; } @@ -2050,7 +2051,7 @@ throw OrthancException(ErrorCode_InternalError); } - switch (resource.type_) + switch (resource.GetLevel()) { case ResourceType_Patient: case ResourceType_Study: @@ -2099,9 +2100,9 @@ target["ModifiedFrom"] = resource.modifiedFrom_; } - if (resource.type_ == ResourceType_Patient || - resource.type_ == ResourceType_Study || - resource.type_ == ResourceType_Series) + if (resource.GetLevel() == ResourceType_Patient || + resource.GetLevel() == ResourceType_Study || + resource.GetLevel() == ResourceType_Series) { target["IsStable"] = resource.isStable_; @@ -2117,15 +2118,15 @@ static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags"; DicomMap mainDicomTags; - resource.tags_.ExtractResourceInformation(mainDicomTags, resource.type_); + resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel()); target[MAIN_DICOM_TAGS] = Json::objectValue; FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format); - if (resource.type_ == ResourceType_Study) + if (resource.GetLevel() == ResourceType_Study) { DicomMap patientMainDicomTags; - resource.tags_.ExtractPatientInformation(patientMainDicomTags); + resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags); target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue; FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format); @@ -2136,13 +2137,23 @@ static const char* const REQUESTED_TAGS = "RequestedTags"; DicomMap tags; - resource.tags_.ExtractTags(tags, requestedTags); + 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::const_iterator it = resource.labels_.begin(); it != resource.labels_.end(); ++it) + { + labels.append(*it); + } + + target["Labels"] = labels; + } } @@ -2153,7 +2164,7 @@ { if (requestedTags.count(DICOM_TAG_INSTANCE_AVAILABILITY) > 0) { - resource.tags_.SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false); + resource.GetMainDicomTags().SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false); resource.missingRequestedTags_.erase(DICOM_TAG_INSTANCE_AVAILABILITY); } } @@ -2171,7 +2182,7 @@ index.GetChildren(instances, seriesPublicId); - resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, + resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, boost::lexical_cast(instances.size()), false); resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); } @@ -2216,13 +2227,13 @@ std::string modalities; Toolbox::JoinStrings(modalities, values, "\\"); - resource.tags_.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false); + resource.GetMainDicomTags().SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false); resource.missingRequestedTags_.erase(DICOM_TAG_MODALITIES_IN_STUDY); } if (hasNbRelatedSeries) { - resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, + resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, boost::lexical_cast(series.size()), false); resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); } @@ -2240,7 +2251,7 @@ if (hasNbRelatedInstances) { - resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, + resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, boost::lexical_cast(instances.size()), false); resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); } @@ -2264,7 +2275,7 @@ { std::string sopClassUids; Toolbox::JoinStrings(sopClassUids, values, "\\"); - resource.tags_.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false); + resource.GetMainDicomTags().SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false); } resource.missingRequestedTags_.erase(DICOM_TAG_SOP_CLASSES_IN_STUDY); @@ -2291,7 +2302,7 @@ if (hasNbRelatedStudies) { - resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, + resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, boost::lexical_cast(studies.size()), false); resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); } @@ -2308,7 +2319,7 @@ if (hasNbRelatedSeries) { - resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, + resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, boost::lexical_cast(series.size()), false); resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); } @@ -2324,7 +2335,7 @@ instances.splice(instances.end(), thisInstancesIds); } - resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, + resource.GetMainDicomTags().SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, boost::lexical_cast(instances.size()), false); resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); } @@ -2388,7 +2399,7 @@ { ExpandedResource resource; - if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceDbFlags_Default, allowStorageAccess)) + if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_Default, allowStorageAccess)) { SerializeExpandedResource(target, resource, format, requestedTags); return true; @@ -2404,57 +2415,63 @@ const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource (if already available) ResourceType level, const std::set& requestedTags, - ExpandResourceDbFlags expandFlags, + ExpandResourceFlags expandFlags, bool allowStorageAccess) { // first try to get the tags from what is already available - if ((expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags) - && (mainDicomTags.GetSize() > 0) - && (dicomAsJson != NULL)) + if ((expandFlags & ExpandResourceFlags_IncludeMainDicomTags) && + mainDicomTags.GetSize() > 0 && + dicomAsJson != NULL) { - resource.tags_.Merge(mainDicomTags); + resource.GetMainDicomTags().Merge(mainDicomTags); if (dicomAsJson->isObject()) { - resource.tags_.FromDicomAsJson(*dicomAsJson); + resource.GetMainDicomTags().FromDicomAsJson(*dicomAsJson); } std::set retrievedTags; std::set missingTags; - resource.tags_.GetTags(retrievedTags); + 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)) { - expandFlags = static_cast(expandFlags & ~ExpandResourceDbFlags_IncludeMainDicomTags); + expandFlags = static_cast(expandFlags & ~ExpandResourceFlags_IncludeMainDicomTags); } - if (missingTags.size() == 0 && expandFlags == ExpandResourceDbFlags_None) // we have already retrieved anything we need + if (missingTags.size() == 0 && expandFlags == ExpandResourceFlags_None) // we have already retrieved anything we need { return true; } } - if (expandFlags != ExpandResourceDbFlags_None - && GetIndex().ExpandResource(resource, publicId, level, requestedTags, static_cast(expandFlags | ExpandResourceDbFlags_IncludeMetadata))) // we always need the metadata to get the mainDicomTagsSignature + if (expandFlags != ExpandResourceFlags_None && + GetIndex().ExpandResource(resource, publicId, level, requestedTags, + static_cast(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.type_)) + if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.GetLevel())) { OrthancConfiguration::ReaderLock lock; if (lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb)) { - LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.type_, false , false) << " has been stored with another version of Main Dicom Tags list, you should POST to /" << Orthanc::GetResourceTypeText(resource.type_, true, false) << "/" << resource.id_ << "/reconstruct to update the list of tags saved in DB. Some MainDicomTags might be missing from this answer."; + 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_)) + if (allowStorageAccess && + !resource.missingRequestedTags_.empty() && + !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_)) { OrthancConfiguration::ReaderLock lock; if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage)) @@ -2472,7 +2489,9 @@ std::string missings; FromDcmtkBridge::FormatListOfTags(missings, missingTags); - LOG(WARNING) << "W001: Accessing Dicom tags from storage when accessing " << Orthanc::GetResourceTypeText(resource.type_, false , false) << " : " << missings; + LOG(WARNING) << "W001: Accessing Dicom tags from storage when accessing " + << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false) + << " : " << missings; } @@ -2508,7 +2527,7 @@ tagsFromJson.FromDicomAsJson(*dicomAsJson, false /* append */, true /* parseSequences*/); } - resource.tags_.Merge(tagsFromJson); + resource.GetMainDicomTags().Merge(tagsFromJson); } // compute the requested tags diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/ServerContext.h Tue Apr 04 17:24:13 2023 +0200 @@ -585,7 +585,7 @@ const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource ResourceType level, const std::set& requestedTags, - ExpandResourceDbFlags expandFlags, + ExpandResourceFlags expandFlags, bool allowStorageAccess); FindStorageAccessMode GetFindStorageAccessMode() const diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp --- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -675,9 +675,12 @@ replacePatientMainDicomTags |= DicomMap::IsMainDicomTag(*it, ResourceType_Patient); } - if ((modificationLevel == ResourceType_Study || modificationLevel == ResourceType_Patient) - && !modification_->IsReplaced(DICOM_TAG_PATIENT_ID) - && modification_->IsKept(DICOM_TAG_STUDY_INSTANCE_UID) && modification_->IsKept(DICOM_TAG_SERIES_INSTANCE_UID) && modification_->IsKept(DICOM_TAG_SOP_INSTANCE_UID)) + if ((modificationLevel == ResourceType_Study || + modificationLevel == ResourceType_Patient) && + !modification_->IsReplaced(DICOM_TAG_PATIENT_ID) && + modification_->IsKept(DICOM_TAG_STUDY_INSTANCE_UID) && + modification_->IsKept(DICOM_TAG_SERIES_INSTANCE_UID) && + modification_->IsKept(DICOM_TAG_SOP_INSTANCE_UID)) { // if we keep the SOPInstanceUID, it very likely means that we are modifying existing resources 'in place' @@ -715,9 +718,9 @@ else { ExpandedResource originalStudy; - if (GetContext().GetIndex().ExpandResource(originalStudy, *studyId, ResourceType_Study, emptyRequestedTags, ExpandResourceDbFlags_IncludeMainDicomTags)) + if (GetContext().GetIndex().ExpandResource(originalStudy, *studyId, ResourceType_Study, emptyRequestedTags, ExpandResourceFlags_IncludeMainDicomTags)) { - targetPatientId = originalStudy.tags_.GetStringValue(DICOM_TAG_PATIENT_ID, "", false); + targetPatientId = originalStudy.GetMainDicomTags().GetStringValue(DICOM_TAG_PATIENT_ID, "", false); } else { @@ -734,7 +737,7 @@ { ExpandedResource targetPatient; - if (GetContext().GetIndex().ExpandResource(targetPatient, lookupPatientResult[0], ResourceType_Patient, emptyRequestedTags, static_cast(ExpandResourceDbFlags_IncludeMainDicomTags | ExpandResourceDbFlags_IncludeChildren))) + if (GetContext().GetIndex().ExpandResource(targetPatient, lookupPatientResult[0], ResourceType_Patient, emptyRequestedTags, static_cast(ExpandResourceFlags_IncludeMainDicomTags | ExpandResourceFlags_IncludeChildren))) { const std::list childrenIds = targetPatient.childrenIds_; bool targetPatientHasOtherStudies = childrenIds.size() > 1; @@ -747,7 +750,7 @@ { // this is allowed if all patient replacedTags do match the target patient tags DicomMap targetPatientTags; - targetPatient.tags_.ExtractPatientInformation(targetPatientTags); + targetPatient.GetMainDicomTags().ExtractPatientInformation(targetPatientTags); std::set mainPatientTags; DicomMap::GetMainDicomTags(mainPatientTags, ResourceType_Patient); @@ -755,9 +758,9 @@ for (std::set::const_iterator mainPatientTag = mainPatientTags.begin(); mainPatientTag != mainPatientTags.end(); ++mainPatientTag) { - if (targetPatientTags.HasTag(*mainPatientTag) - && (!modification_->IsReplaced(*mainPatientTag) - || modification_->GetReplacementAsString(*mainPatientTag) != targetPatientTags.GetStringValue(*mainPatientTag, "", false))) + if (targetPatientTags.HasTag(*mainPatientTag) && + (!modification_->IsReplaced(*mainPatientTag) || + modification_->GetReplacementAsString(*mainPatientTag) != targetPatientTags.GetStringValue(*mainPatientTag, "", false))) { throw OrthancException(ErrorCode_BadRequest, std::string("Trying to change patient tags in a study. The Patient already exists and has other studies. All the 'Replace' tags should match the existing patient main dicom tags. Try using /patients/../modify instead to modify the patient. Failing tag: ") + mainPatientTag->Format()); } @@ -769,8 +772,7 @@ } } } - } - + } } } } diff -r bd25d1c33362 -r 7363b6e7edf5 OrthancServer/UnitTestsSources/ServerIndexTests.cpp --- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Apr 04 17:23:55 2023 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Apr 04 17:24:13 2023 +0200 @@ -167,8 +167,9 @@ std::vector lookup; lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); - - transaction_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */); + + std::set noLabel; + transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, noLabel, 0 /* no limit */); } void DoLookupIdentifier2(std::list& result, @@ -188,7 +189,8 @@ lookup.push_back(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); - transaction_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */); + std::set noLabel; + transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, noLabel, 0 /* no limit */); } }; }