# HG changeset patch # User Sebastien Jodogne # Date 1680547994 -7200 # Node ID d0f7c742d3970c79a5a204a2fd155421278d3aff # Parent df39c7583a4907b77e1799a8ed8ab07a4910fa39 started implementation of labels diff -r df39c7583a49 -r d0f7c742d397 NEWS --- a/NEWS Mon Apr 03 18:09:04 2023 +0200 +++ b/NEWS Mon Apr 03 20:53:14 2023 +0200 @@ -1,11 +1,19 @@ 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 +* The "/patients/{id}", "/studies/{id}", "/series/{id}" and "/instances/{id}" + contain the "Labels" field +* "/system": added "UserMetadata" Plugins ------- @@ -18,7 +26,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 @@ -34,7 +42,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. @@ -145,7 +153,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 df39c7583a49 -r d0f7c742d397 OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/CMakeLists.txt Mon Apr 03 20:53:14 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 df39c7583a49 -r d0f7c742d397 OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Mon Apr 03 20:53:14 2023 +0200 @@ -1437,8 +1437,8 @@ } - virtual void GetLabels(std::set& target, - int64_t resource) ORTHANC_OVERRIDE + virtual void ListLabels(std::set& target, + int64_t resource) ORTHANC_OVERRIDE { throw OrthancException(ErrorCode_InternalError); // Not supported } diff -r df39c7583a49 -r d0f7c742d397 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Mon Apr 03 20:53:14 2023 +0200 @@ -1051,8 +1051,8 @@ } - virtual void GetLabels(std::set& target, - int64_t resource) ORTHANC_OVERRIDE + virtual void ListLabels(std::set& target, + int64_t resource) ORTHANC_OVERRIDE { throw OrthancException(ErrorCode_InternalError); // Not supported } diff -r df39c7583a49 -r d0f7c742d397 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Mon Apr 03 20:53:14 2023 +0200 @@ -1163,8 +1163,8 @@ } - virtual void GetLabels(std::set& target, - int64_t resource) ORTHANC_OVERRIDE + virtual void ListLabels(std::set& target, + int64_t resource) ORTHANC_OVERRIDE { throw OrthancException(ErrorCode_NotImplemented); } diff -r df39c7583a49 -r d0f7c742d397 OrthancServer/Sources/Database/IDatabaseWrapper.h --- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Mon Apr 03 20:53:14 2023 +0200 @@ -250,8 +250,8 @@ virtual void RemoveLabel(int64_t resource, const std::string& label) = 0; - virtual void GetLabels(std::set& target, - int64_t resource) = 0; + virtual void ListLabels(std::set& target, + int64_t resource) = 0; }; diff -r df39c7583a49 -r d0f7c742d397 OrthancServer/Sources/Database/InstallLabelsTable.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallLabelsTable.sql Mon Apr 03 20:53:14 2023 +0200 @@ -0,0 +1,24 @@ +-- 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 + ); diff -r df39c7583a49 -r d0f7c742d397 OrthancServer/Sources/Database/PrepareDatabase.sql --- a/OrthancServer/Sources/Database/PrepareDatabase.sql Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Mon Apr 03 20:53:14 2023 +0200 @@ -91,6 +91,12 @@ 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 + ); + CREATE INDEX ChildrenIndex ON Resources(parentId); CREATE INDEX PublicIndex ON Resources(publicId); CREATE INDEX ResourceTypeIndex ON Resources(resourceType); diff -r df39c7583a49 -r d0f7c742d397 OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Mon Apr 03 20:53:14 2023 +0200 @@ -1084,21 +1084,50 @@ virtual void AddLabel(int64_t resource, const std::string& label) ORTHANC_OVERRIDE { - throw OrthancException(ErrorCode_NotImplemented); + if (label.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT 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 { - throw OrthancException(ErrorCode_NotImplemented); + 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 GetLabels(std::set& target, - int64_t resource) ORTHANC_OVERRIDE + virtual void ListLabels(std::set& target, + int64_t resource) ORTHANC_OVERRIDE { - throw OrthancException(ErrorCode_NotImplemented); + 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)); + } } }; @@ -1373,9 +1402,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") { @@ -1384,6 +1413,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 df39c7583a49 -r d0f7c742d397 OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Mon Apr 03 20:53:14 2023 +0200 @@ -939,6 +939,11 @@ } } + if (expandFlags & ExpandResourceDbFlags_IncludeLabels) + { + transaction.ListLabels(target.labels_, internalId); + } + std::string tmp; if (LookupStringMetadata(tmp, target.metadata_, MetadataType_AnonymizedFrom)) @@ -3519,4 +3524,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 df39c7583a49 -r d0f7c742d397 OrthancServer/Sources/Database/StatelessDatabaseOperations.h --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Mon Apr 03 20:53:14 2023 +0200 @@ -62,6 +62,9 @@ size_t fileSize_; std::string fileUuid_; int indexInSeries_; + + // New in Orthanc 1.12.0 + std::set labels_; }; enum ExpandResourceDbFlags @@ -70,10 +73,12 @@ ExpandResourceDbFlags_IncludeMetadata = (1 << 0), ExpandResourceDbFlags_IncludeChildren = (1 << 1), ExpandResourceDbFlags_IncludeMainDicomTags = (1 << 2), + ExpandResourceDbFlags_IncludeLabels = (1 << 3), ExpandResourceDbFlags_Default = (ExpandResourceDbFlags_IncludeMetadata | ExpandResourceDbFlags_IncludeChildren | - ExpandResourceDbFlags_IncludeMainDicomTags) + ExpandResourceDbFlags_IncludeMainDicomTags | + ExpandResourceDbFlags_IncludeLabels) }; class StatelessDatabaseOperations : public boost::noncopyable @@ -82,6 +87,12 @@ typedef std::list Attachments; typedef std::map, std::string> MetadataMap; + enum LabelOperation + { + LabelOperation_Add, + LabelOperation_Remove + }; + class ITransactionContext : public IDatabaseListener { public: @@ -312,6 +323,12 @@ { return transaction_.LookupResourceAndParent(id, type, parentPublicId, publicId); } + + void ListLabels(std::set& target, + int64_t id) + { + transaction_.ListLabels(target, id); + } }; @@ -424,6 +441,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); + } }; @@ -687,5 +716,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 df39c7583a49 -r d0f7c742d397 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Apr 03 20:53:14 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 ----------------------------------------------- @@ -3854,6 +3977,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 df39c7583a49 -r d0f7c742d397 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Mon Apr 03 18:09:04 2023 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Mon Apr 03 20:53:14 2023 +0200 @@ -2144,6 +2144,16 @@ } + { + 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; + } }