Mercurial > hg > orthanc
changeset 5269:c3f425e78539
integration db-protobuf->mainline
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 12 Apr 2023 18:08:45 +0200 |
parents | 043bf0958614 (current diff) cdeb7f027c1b (diff) |
children | c5f243ad7103 |
files | |
diffstat | 38 files changed, 1532 insertions(+), 285 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Wed Apr 12 18:04:05 2023 +0200 +++ b/NEWS Wed Apr 12 18:08:45 2023 +0200 @@ -1,11 +1,21 @@ 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 +* "/patients/{id}", "/studies/{id}", "/series/{id}" and "/instances/{id}" + contain the "Labels" field +* "/tools/find" now accepts the "Labels" and "LabelsConstraint" arguments +* "/tools/labels" lists all the labels that are associated with any resource +* "/system": added "UserMetadata" and "HasLabels" Plugins ------- @@ -16,6 +26,7 @@ Orthanc Explorer ---------------- +* Added support for labels * Added buttons to copy the URL of ZIP archives and DICOM files to the clipboard Maintenance @@ -23,7 +34,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. @@ -45,7 +56,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. @@ -156,7 +167,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 --------
--- a/OrthancFramework/Sources/SerializationToolbox.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancFramework/Sources/SerializationToolbox.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -72,6 +72,21 @@ } + std::string SerializationToolbox::ReadString(const Json::Value& value, + const std::string& field, + const std::string& defaultValue) + { + if (value.isMember(field.c_str())) + { + return ReadString(value, field); + } + else + { + return defaultValue; + } + } + + int SerializationToolbox::ReadInteger(const Json::Value& value, const std::string& field) {
--- a/OrthancFramework/Sources/SerializationToolbox.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancFramework/Sources/SerializationToolbox.h Wed Apr 12 18:08:45 2023 +0200 @@ -38,6 +38,10 @@ static std::string ReadString(const Json::Value& value, const std::string& field); + static std::string ReadString(const Json::Value& value, + const std::string& field, + const std::string& defaultValue); + static int ReadInteger(const Json::Value& value, const std::string& field);
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Wed Apr 12 18:08:45 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")); }
--- a/OrthancServer/CMakeLists.txt Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/CMakeLists.txt Wed Apr 12 18:08:45 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)
--- a/OrthancServer/OrthancExplorer/explorer.css Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/OrthancExplorer/explorer.css Wed Apr 12 18:08:45 2023 +0200 @@ -63,3 +63,22 @@ .switch-container .ui-slider-switch { width: 100%; } + +.label { + display: inline-block; + background-color: gray; + margin: 5px; + padding: 5px; + border-radius: 10px; +} + +.label button { + background-color: transparent; + border: 0px; + cursor: pointer; + border-radius: 10px; +} + +.label button:hover { + background-color: lightgray; +}
--- a/OrthancServer/OrthancExplorer/explorer.html Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/OrthancExplorer/explorer.html Wed Apr 12 18:08:45 2023 +0200 @@ -81,6 +81,11 @@ <input type="text" name="lookup-study-description" id="lookup-study-description" value="" /> </div> + <div data-role="fieldcontain" id="lookup-study-labels-div"> + <label for="lookup-study-labels">Labels:</label> + <input type="text" name="lookup-study-description" id="lookup-study-labels" value="" /> + </div> + <div data-role="fieldcontain"> <label for="lookup-study-date">Study Date:</label> <select name="lookup-study-date" id="lookup-study-date"> @@ -429,8 +434,8 @@ <a href="#" id="instance-modified-from">Before modification</a> </li> <li data-icon="arrow-d"><a href="#" id="instance-download-dicom">Download the DICOM file</a></li> - <li data-icon="arrow-d"><a href="#" id="instance-download-link">Copy link to the DICOM file</a></li> <li data-icon="arrow-d"><a href="#" id="instance-download-json">Download the JSON file</a></li> + <li data-icon="gear"><a href="#" id="instance-download-link">Copy link to the DICOM file</a></li> <li data-icon="search"><a href="#" id="instance-preview">Preview the instance</a></li> </ul> </div>
--- a/OrthancServer/OrthancExplorer/explorer.js Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/OrthancExplorer/explorer.js Wed Apr 12 18:08:45 2023 +0200 @@ -65,6 +65,12 @@ var MODIFIED_FROM = 'ModifiedFrom'; +function IsAlphanumeric(s) +{ + return s.match(/^[0-9a-zA-Z]+$/); +} + + function DeepCopy(obj) { return jQuery.extend(true, {}, obj); @@ -479,6 +485,14 @@ } else { $('.warning-insecure').hide(); } + + // New in Orthanc 1.12.0 + if ('HasLabels' in s && + s.HasLabels) { + $('#lookup-study-labels-div').show(); + } else { + $('#lookup-study-labels-div').hide(); + } } }); }); @@ -550,6 +564,10 @@ else if (input.id == 'lookup-study-date-specific') { // Ignore } + else if (input.id == 'lookup-study-labels') { + // New in Orthanc 1.12.0 + lookup['Labels'] = input.value.split(' '); + } else { console.error('Unknown lookup field: ' + input.id); } @@ -701,9 +719,94 @@ } target.listview('refresh'); }); +} + + +function RefreshLabels(nodeLabels, resourceLevel, resourceId) +{ + GetResource('/' + resourceLevel + '/' + resourceId + '/labels', function(labels) { + nodeLabels.empty(); + + if (labels.length > 0) { + nodeLabels.css('display', 'block'); + + for (var i = 0; i < labels.length; i++) { + var removeButton = $('<button>').text('X').attr('title', 'Remove label "' + labels[i] + '"'); + + removeButton.click({ + label : labels[i], + nodeLabels : nodeLabels + }, function(s) { + $.ajax({ + url: '../' + resourceLevel + '/' + resourceId + '/labels/' + s.data.label, + dataType: 'json', + type: 'DELETE', + success: function(ss) { + RefreshLabels(nodeLabels, resourceLevel, resourceId); + } + }); + }); + + nodeLabels.append($('<span>').text(labels[i] + ' ').addClass('label').append(removeButton)); + } + } else { + nodeLabels.css('display', 'none'); + } + }); } + +function ConfigureLabels(target, system, resourceLevel, resourceId) +{ + if (system.HasLabels === true) { + var nodeLabels = $('<li>').append($('<div>')); + var addLabelButton = $('<a>').text('Add label'); + + RefreshLabels(nodeLabels, resourceLevel, resourceId); + + addLabelButton.click(function(s) { + $('#dialog').simpledialog2({ + mode: 'button', + animate: false, + headerText: 'Add label', + headerClose: true, + buttonPrommpt: 'Enter the new label', + buttonInput: true, + width: '100%', + buttons : { + 'OK': { + click: function () { + var label = $.mobile.sdLastInput; + if (label.length > 0) { + if (IsAlphanumeric(label)) { + $.ajax({ + url: '../' + resourceLevel + '/' + resourceId + '/labels/' + label, + dataType: 'json', + type: 'PUT', + data: '', + success: function(ss) { + RefreshLabels(nodeLabels, resourceLevel, resourceId); + } + }); + } else { + alert('Error: Labels can only contain alphanumeric characters'); + } + } + } + }, + } + }); + }); + + target + .append('<li data-role="list-divider">Labels</li>') + .append(nodeLabels) + .append($('<li>').attr('data-icon', 'plus').append(addLabelButton)); + } +} + + function RefreshPatient() { var pageData, target, v; @@ -711,53 +814,57 @@ if ($.mobile.pageData) { pageData = DeepCopy($.mobile.pageData); - GetResource('/patients/' + pageData.uuid + '?full', function(patient) { - GetResource('/patients/' + pageData.uuid + '/studies?full', function(studies) { - SortOnDicomTag(studies, STUDY_DATE, false, true); + GetResource('/system', function(system) { + GetResource('/patients/' + pageData.uuid + '?full', function(patient) { + GetResource('/patients/' + pageData.uuid + '/studies?full', function(studies) { + SortOnDicomTag(studies, STUDY_DATE, false, true); - $('#patient-info li').remove(); - $('#patient-info') - .append('<li data-role="list-divider">Patient</li>') - .append(FormatPatient(patient)) - .listview('refresh'); + $('#patient-info li').remove(); + + var info = $('#patient-info') + .append('<li data-role="list-divider">Patient</li>') + .append(FormatPatient(patient)); + ConfigureLabels(info, system, 'patients', patient.ID); + info.listview('refresh'); - target = $('#list-studies'); - $('li', target).remove(); - - for (var i = 0; i < studies.length; i++) { - if (i == 0 || - GetMainDicomTag(studies[i].MainDicomTags, STUDY_DATE) != - GetMainDicomTag(studies[i - 1].MainDicomTags, STUDY_DATE)) - { - target.append($('<li>') - .attr('data-role', 'list-divider') - .text(FormatDicomDate(GetMainDicomTag(studies[i].MainDicomTags, STUDY_DATE)))); + target = $('#list-studies'); + $('li', target).remove(); + + for (var i = 0; i < studies.length; i++) { + if (i == 0 || + GetMainDicomTag(studies[i].MainDicomTags, STUDY_DATE) != + GetMainDicomTag(studies[i - 1].MainDicomTags, STUDY_DATE)) + { + target.append($('<li>') + .attr('data-role', 'list-divider') + .text(FormatDicomDate(GetMainDicomTag(studies[i].MainDicomTags, STUDY_DATE)))); + } + + target.append(FormatStudy(studies[i], '#study?uuid=' + studies[i].ID)); } - target.append(FormatStudy(studies[i], '#study?uuid=' + studies[i].ID)); - } + SetupAnonymizedOrModifiedFrom('#patient-anonymized-from', patient, 'patient', ANONYMIZED_FROM); + SetupAnonymizedOrModifiedFrom('#patient-modified-from', patient, 'patient', MODIFIED_FROM); + SetupAttachments('#patient-access', 'patient-attachment', pageData.uuid, 'patients'); - SetupAnonymizedOrModifiedFrom('#patient-anonymized-from', patient, 'patient', ANONYMIZED_FROM); - SetupAnonymizedOrModifiedFrom('#patient-modified-from', patient, 'patient', MODIFIED_FROM); - SetupAttachments('#patient-access', 'patient-attachment', pageData.uuid, 'patients'); - - target.listview('refresh'); + target.listview('refresh'); - // Check whether this patient is protected - $.ajax({ - url: '../patients/' + pageData.uuid + '/protected', - type: 'GET', - dataType: 'text', - async: false, - cache: false, - success: function (s) { - v = (s == '1') ? 'on' : 'off'; - $('#protection').val(v).slider('refresh'); - } + // Check whether this patient is protected + $.ajax({ + url: '../patients/' + pageData.uuid + '/protected', + type: 'GET', + dataType: 'text', + async: false, + cache: false, + success: function (s) { + v = (s == '1') ? 'on' : 'off'; + $('#protection').val(v).slider('refresh'); + } + }); + + currentPage = 'patient'; + currentUuid = pageData.uuid; }); - - currentPage = 'patient'; - currentUuid = pageData.uuid; }); }); } @@ -771,43 +878,47 @@ if ($.mobile.pageData) { pageData = DeepCopy($.mobile.pageData); - GetResource('/studies/' + pageData.uuid + '?full', function(study) { - GetResource('/patients/' + study.ParentPatient + '?full', function(patient) { - GetResource('/studies/' + pageData.uuid + '/series?full', function(series) { - SortOnDicomTag(series, SERIES_DATE, false, true); + GetResource('/system', function(system) { + GetResource('/studies/' + pageData.uuid + '?full', function(study) { + GetResource('/patients/' + study.ParentPatient + '?full', function(patient) { + GetResource('/studies/' + pageData.uuid + '/series?full', function(series) { + SortOnDicomTag(series, SERIES_DATE, false, true); + + $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID); + $('#study-info li').remove(); - $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID); - $('#study-info li').remove(); - $('#study-info') - .append('<li data-role="list-divider">Patient</li>') - .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true)) - .append('<li data-role="list-divider">Study</li>') - .append(FormatStudy(study)) - .listview('refresh'); + var info = $('#study-info') + .append('<li data-role="list-divider">Patient</li>') + .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true)) + .append('<li data-role="list-divider">Study</li>') + .append(FormatStudy(study)); + ConfigureLabels(info, system, 'studies', study.ID); + info.listview('refresh'); - SetupAnonymizedOrModifiedFrom('#study-anonymized-from', study, 'study', ANONYMIZED_FROM); - SetupAnonymizedOrModifiedFrom('#study-modified-from', study, 'study', MODIFIED_FROM); - SetupAttachments('#study-access', 'study-attachment', pageData.uuid, 'studies'); + SetupAnonymizedOrModifiedFrom('#study-anonymized-from', study, 'study', ANONYMIZED_FROM); + SetupAnonymizedOrModifiedFrom('#study-modified-from', study, 'study', MODIFIED_FROM); + SetupAttachments('#study-access', 'study-attachment', pageData.uuid, 'studies'); - target = $('#list-series'); - $('li', target).remove(); - for (var i = 0; i < series.length; i++) { - if (i == 0 || - GetMainDicomTag(series[i].MainDicomTags, SERIES_DATE) != - GetMainDicomTag(series[i - 1].MainDicomTags, SERIES_DATE)) - { - target.append($('<li>') - .attr('data-role', 'list-divider') - .text(FormatDicomDate(GetMainDicomTag(series[i].MainDicomTags, SERIES_DATE)))); + target = $('#list-series'); + $('li', target).remove(); + for (var i = 0; i < series.length; i++) { + if (i == 0 || + GetMainDicomTag(series[i].MainDicomTags, SERIES_DATE) != + GetMainDicomTag(series[i - 1].MainDicomTags, SERIES_DATE)) + { + target.append($('<li>') + .attr('data-role', 'list-divider') + .text(FormatDicomDate(GetMainDicomTag(series[i].MainDicomTags, SERIES_DATE)))); + } + + target.append(FormatSeries(series[i], '#series?uuid=' + series[i].ID)); } - - target.append(FormatSeries(series[i], '#series?uuid=' + series[i].ID)); - } - target.listview('refresh'); + target.listview('refresh'); - currentPage = 'study'; - currentUuid = pageData.uuid; + currentPage = 'study'; + currentUuid = pageData.uuid; + }); }); }); }); @@ -822,38 +933,41 @@ if ($.mobile.pageData) { pageData = DeepCopy($.mobile.pageData); - GetResource('/series/' + pageData.uuid + '?full', function(series) { - GetResource('/studies/' + series.ParentStudy + '?full', function(study) { - GetResource('/patients/' + study.ParentPatient + '?full', function(patient) { - GetResource('/series/' + pageData.uuid + '/instances?full', function(instances) { - Sort(instances, function(x) { return x.IndexInSeries; }, true, false); + GetResource('/system', function(system) { + GetResource('/series/' + pageData.uuid + '?full', function(series) { + GetResource('/studies/' + series.ParentStudy + '?full', function(study) { + GetResource('/patients/' + study.ParentPatient + '?full', function(patient) { + GetResource('/series/' + pageData.uuid + '/instances?full', function(instances) { + Sort(instances, function(x) { return x.IndexInSeries; }, true, false); - $('#series .patient-link').attr('href', '#patient?uuid=' + patient.ID); - $('#series .study-link').attr('href', '#study?uuid=' + study.ID); + $('#series .patient-link').attr('href', '#patient?uuid=' + patient.ID); + $('#series .study-link').attr('href', '#study?uuid=' + study.ID); - $('#series-info li').remove(); - $('#series-info') - .append('<li data-role="list-divider">Patient</li>') - .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true)) - .append('<li data-role="list-divider">Study</li>') - .append(FormatStudy(study, '#study?uuid=' + study.ID, true)) - .append('<li data-role="list-divider">Series</li>') - .append(FormatSeries(series)) - .listview('refresh'); + $('#series-info li').remove(); + var info = $('#series-info') + .append('<li data-role="list-divider">Patient</li>') + .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true)) + .append('<li data-role="list-divider">Study</li>') + .append(FormatStudy(study, '#study?uuid=' + study.ID, true)) + .append('<li data-role="list-divider">Series</li>') + .append(FormatSeries(series)); + ConfigureLabels(info, system, 'series', series.ID); + info.listview('refresh'); - SetupAnonymizedOrModifiedFrom('#series-anonymized-from', series, 'series', ANONYMIZED_FROM); - SetupAnonymizedOrModifiedFrom('#series-modified-from', series, 'series', MODIFIED_FROM); - SetupAttachments('#series-access', 'series-attachment', pageData.uuid, 'series'); + SetupAnonymizedOrModifiedFrom('#series-anonymized-from', series, 'series', ANONYMIZED_FROM); + SetupAnonymizedOrModifiedFrom('#series-modified-from', series, 'series', MODIFIED_FROM); + SetupAttachments('#series-access', 'series-attachment', pageData.uuid, 'series'); - target = $('#list-instances'); - $('li', target).remove(); - for (var i = 0; i < instances.length; i++) { - target.append(FormatInstance(instances[i], '#instance?uuid=' + instances[i].ID)); - } - target.listview('refresh'); + target = $('#list-instances'); + $('li', target).remove(); + for (var i = 0; i < instances.length; i++) { + target.append(FormatInstance(instances[i], '#instance?uuid=' + instances[i].ID)); + } + target.listview('refresh'); - currentPage = 'series'; - currentUuid = pageData.uuid; + currentPage = 'series'; + currentUuid = pageData.uuid; + }); }); }); }); @@ -935,51 +1049,54 @@ if ($.mobile.pageData) { pageData = DeepCopy($.mobile.pageData); - GetResource('/instances/' + pageData.uuid + '?full', function(instance) { - GetResource('/series/' + instance.ParentSeries + '?full', function(series) { - GetResource('/studies/' + series.ParentStudy + '?full', function(study) { - GetResource('/patients/' + study.ParentPatient + '?full', function(patient) { + GetResource('/system', function(system) { + GetResource('/instances/' + pageData.uuid + '?full', function(instance) { + GetResource('/series/' + instance.ParentSeries + '?full', function(series) { + GetResource('/studies/' + series.ParentStudy + '?full', function(study) { + GetResource('/patients/' + study.ParentPatient + '?full', function(patient) { - $('#instance .patient-link').attr('href', '#patient?uuid=' + patient.ID); - $('#instance .study-link').attr('href', '#study?uuid=' + study.ID); - $('#instance .series-link').attr('href', '#series?uuid=' + series.ID); - - $('#instance-info li').remove(); - $('#instance-info') - .append('<li data-role="list-divider">Patient</li>') - .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true)) - .append('<li data-role="list-divider">Study</li>') - .append(FormatStudy(study, '#study?uuid=' + study.ID, true)) - .append('<li data-role="list-divider">Series</li>') - .append(FormatSeries(series, '#series?uuid=' + series.ID, true)) - .append('<li data-role="list-divider">Instance</li>') - .append(FormatInstance(instance)) - .listview('refresh'); + $('#instance .patient-link').attr('href', '#patient?uuid=' + patient.ID); + $('#instance .study-link').attr('href', '#study?uuid=' + study.ID); + $('#instance .series-link').attr('href', '#series?uuid=' + series.ID); + + $('#instance-info li').remove(); + var info = $('#instance-info') + .append('<li data-role="list-divider">Patient</li>') + .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true)) + .append('<li data-role="list-divider">Study</li>') + .append(FormatStudy(study, '#study?uuid=' + study.ID, true)) + .append('<li data-role="list-divider">Series</li>') + .append(FormatSeries(series, '#series?uuid=' + series.ID, true)) + .append('<li data-role="list-divider">Instance</li>') + .append(FormatInstance(instance)); + ConfigureLabels(info, system, 'instances', instance.ID); + info.listview('refresh'); - GetResource('/instances/' + instance.ID + '/tags', function(s) { - $('#dicom-tree').tree('loadData', ConvertForTree(s)); - }); + GetResource('/instances/' + instance.ID + '/tags', function(s) { + $('#dicom-tree').tree('loadData', ConvertForTree(s)); + }); - GetResource('/instances/' + instance.ID + '/header', function(s) { - $('#dicom-metaheader').tree('loadData', ConvertForTree(s)); - }); + GetResource('/instances/' + instance.ID + '/header', function(s) { + $('#dicom-metaheader').tree('loadData', ConvertForTree(s)); + }); - $('#transfer-syntax').hide(); - GetResource('/instances/' + instance.ID + '/metadata?expand', function(s) { - transferSyntax = s['TransferSyntax']; - if (transferSyntax !== undefined) { - $('#transfer-syntax').show(); - $('#transfer-syntax-text').text(transferSyntax); - } + $('#transfer-syntax').hide(); + GetResource('/instances/' + instance.ID + '/metadata?expand', function(s) { + transferSyntax = s['TransferSyntax']; + if (transferSyntax !== undefined) { + $('#transfer-syntax').show(); + $('#transfer-syntax-text').text(transferSyntax); + } + }); + + SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', ANONYMIZED_FROM); + SetupAnonymizedOrModifiedFrom('#instance-modified-from', instance, 'instance', MODIFIED_FROM); + + SetupAttachments('#instance-access', 'instance-attachment', pageData.uuid, 'instances'); + + currentPage = 'instance'; + currentUuid = pageData.uuid; }); - - SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', ANONYMIZED_FROM); - SetupAnonymizedOrModifiedFrom('#instance-modified-from', instance, 'instance', MODIFIED_FROM); - - SetupAttachments('#instance-access', 'instance-attachment', pageData.uuid, 'instances'); - - currentPage = 'instance'; - currentUuid = pageData.uuid; }); }); });
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -565,8 +565,15 @@ std::list<std::string>* instancesId, const std::vector<DatabaseConstraint>& lookup, ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, uint32_t limit) ORTHANC_OVERRIDE { + if (!labels.empty()) + { + throw OrthancException(ErrorCode_InternalError); // "HasLabelsSupport()" has returned "false" + } + if (that_.extensions_.lookupResources == NULL) { // Fallback to compatibility mode @@ -1413,6 +1420,33 @@ 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<std::string>& target, + int64_t resource) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + + virtual void ListAllLabels(std::set<std::string>& target) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } };
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Wed Apr 12 18:08:45 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); }; }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -800,8 +800,15 @@ std::list<std::string>* instancesId, // Can be NULL if not needed const std::vector<DatabaseConstraint>& lookup, ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, uint32_t limit) ORTHANC_OVERRIDE { + if (!labels.empty()) + { + throw OrthancException(ErrorCode_InternalError); // "HasLabelsSupport()" has returned "false" + } + std::vector<OrthancPluginDatabaseConstraint> constraints; std::vector< std::vector<const char*> > constraintsValues; @@ -1027,6 +1034,33 @@ 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<std::string>& target, + int64_t resource) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + + virtual void ListAllLabels(std::set<std::string>& target) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } };
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Wed Apr 12 18:08:45 2023 +0200 @@ -82,6 +82,11 @@ IStorageArea& storageArea) ORTHANC_OVERRIDE; virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE; + + virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE + { + return false; + } }; }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -27,10 +27,12 @@ # error The plugin support is disabled #endif +#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/OrthancException.h" #include "../../Sources/Database/ResourcesContent.h" #include "../../Sources/Database/VoidDatabaseListener.h" +#include "../../Sources/ServerToolbox.h" #include "PluginsEnumerations.h" #include "OrthancDatabasePlugin.pb.h" // Auto-generated file @@ -223,6 +225,33 @@ } + void ListLabelsInternal(std::set<std::string>& target, + bool isSingleResource, + int64_t resource) + { + if (database_.HasLabelsSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_list_labels()->set_single_resource(isSingleResource); + request.mutable_list_labels()->set_id(resource); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LIST_LABELS, request); + + target.clear(); + for (int i = 0; i < response.list_labels().labels().size(); i++) + { + target.insert(response.list_labels().labels(i)); + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + public: Transaction(OrthancPluginDatabaseV4& database, IDatabaseListener& listener, @@ -915,8 +944,16 @@ std::list<std::string>* instancesId, // Can be NULL if not needed const std::vector<DatabaseConstraint>& lookup, ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, uint32_t limit) ORTHANC_OVERRIDE { + if (!database_.HasLabelsSupport() && + !labels.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 +1003,29 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + request.mutable_lookup_resources()->add_labels(*it); + } + + switch (labelsConstraint) + { + case LabelsConstraint_All: + request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ALL); + break; + + case LabelsConstraint_Any: + request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ANY); + break; + + case LabelsConstraint_None: + request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_NONE); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } DatabasePluginMessages::TransactionResponse response; ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES, request); @@ -1132,6 +1192,57 @@ return false; } } + + + virtual void AddLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + if (database_.HasLabelsSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_add_label()->set_id(resource); + request.mutable_add_label()->set_label(label); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_LABEL, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + + virtual void RemoveLabel(int64_t resource, + const std::string& label) ORTHANC_OVERRIDE + { + if (database_.HasLabelsSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_remove_label()->set_id(resource); + request.mutable_remove_label()->set_label(label); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_REMOVE_LABEL, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + + virtual void ListLabels(std::set<std::string>& target, + int64_t resource) ORTHANC_OVERRIDE + { + ListLabelsInternal(target, true, resource); + } + + + virtual void ListAllLabels(std::set<std::string>& target) ORTHANC_OVERRIDE + { + ListLabelsInternal(target, false, -1); + } }; @@ -1146,7 +1257,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 << "\""; @@ -1165,6 +1277,31 @@ definition_.finalize(definition_.backend); } + + static void AddIdentifierTags(DatabasePluginMessages::Open::Request& request, + ResourceType level) + { + const DicomTag* tags = NULL; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + if (tags == NULL || + size == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + for (size_t i = 0; i < size; i++) + { + DatabasePluginMessages::Open_Request_IdentifierTag* tag = request.add_identifier_tags(); + tag->set_level(Convert(level)); + tag->set_group(tags[i].GetGroup()); + tag->set_element(tags[i].GetElement()); + tag->set_name(FromDcmtkBridge::GetTagName(tags[i], "")); + } + } + void OrthancPluginDatabaseV4::Open() { @@ -1175,6 +1312,11 @@ { DatabasePluginMessages::DatabaseRequest request; + AddIdentifierTags(*request.mutable_open(), ResourceType_Patient); + AddIdentifierTags(*request.mutable_open(), ResourceType_Study); + AddIdentifierTags(*request.mutable_open(), ResourceType_Series); + AddIdentifierTags(*request.mutable_open(), ResourceType_Instance); + DatabasePluginMessages::DatabaseResponse response; ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_OPEN, request); } @@ -1186,9 +1328,10 @@ 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; + open_ = true; } @@ -1307,4 +1450,17 @@ return hasRevisionsSupport_; } } + + + bool OrthancPluginDatabaseV4::HasLabelsSupport() const + { + if (!open_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return hasLabelsSupport_; + } + } }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Wed Apr 12 18:08:45 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; }; }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Wed Apr 12 18:08:45 2023 +0200 @@ -71,6 +71,12 @@ CONSTRAINT_LIST = 4; } +enum LabelsConstraintType { + LABELS_CONSTRAINT_ALL = 0; + LABELS_CONSTRAINT_ANY = 1; + LABELS_CONSTRAINT_NONE = 2; +} + message ServerIndexChange { int64 seq = 1; int32 change_type = 2; // opaque "ChangeType" in Orthanc @@ -129,11 +135,19 @@ uint32 database_version = 1; bool supports_flush_to_disk = 2; bool supports_revisions = 3; + bool supports_labels = 4; } } message Open { message Request { + message IdentifierTag { + ResourceType level = 1; + uint32 group = 2; + uint32 element = 3; + string name = 4; + } + repeated IdentifierTag identifier_tags = 1; } message Response { } @@ -258,6 +272,9 @@ OPERATION_GET_CHILDREN_METADATA = 42; OPERATION_GET_LAST_CHANGE_INDEX = 43; OPERATION_LOOKUP_RESOURCE_AND_PARENT = 44; + OPERATION_ADD_LABEL = 45; // New in Orthanc 1.12.0 + OPERATION_REMOVE_LABEL = 46; // New in Orthanc 1.12.0 + OPERATION_LIST_LABELS = 47; // New in Orthanc 1.12.0 } message Rollback { @@ -654,6 +671,8 @@ ResourceType query_level = 2; uint32 limit = 3; bool retrieve_instances_ids = 4; + repeated string labels = 5; // New in Orthanc 1.12.0 + LabelsConstraintType labels_constraint = 6; // New in Orthanc 1.12.0 } message Response { repeated string resources_ids = 1; @@ -735,6 +754,34 @@ } } +message AddLabel { + message Request { + int64 id = 1; + string label = 2; + } + message Response { + } +} + +message RemoveLabel { + message Request { + int64 id = 1; + string label = 2; + } + message Response { + } +} + +message ListLabels { + message Request { + bool single_resource = 1; + int64 id = 2; // Only if "single_resource" is "true" + } + message Response { + repeated string labels = 1; + } +} + message TransactionRequest { sfixed64 transaction = 1; TransactionOperation operation = 2; @@ -784,6 +831,9 @@ GetChildrenMetadata.Request get_children_metadata = 142; GetLastChangeIndex.Request get_last_change_index = 143; LookupResourceAndParent.Request lookup_resource_and_parent = 144; + AddLabel.Request add_label = 145; + RemoveLabel.Request remove_label = 146; + ListLabels.Request list_labels = 147; } message TransactionResponse { @@ -832,6 +882,9 @@ GetChildrenMetadata.Response get_children_metadata = 142; GetLastChangeIndex.Response get_last_change_index = 143; LookupResourceAndParent.Response lookup_resource_and_parent = 144; + AddLabel.Response add_label = 145; + RemoveLabel.Response remove_label = 146; + ListLabels.Response list_labels = 147; } enum RequestType {
--- a/OrthancServer/Resources/Configuration.json Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Resources/Configuration.json Wed Apr 12 18:08:45 2023 +0200 @@ -433,8 +433,8 @@ // "AllowStore" : true, // "AllowStorageCommitment" : false, // new in 1.6.0 // "AllowTranscoding" : true, // new in 1.7.0 - // "UseDicomTls" : false // new in 1.9.0 - // "LocalAet" : "HELLO" // new in 1.9.0 + // "UseDicomTls" : false, // new in 1.9.0 + // "LocalAet" : "HELLO", // new in 1.9.0 // "Timeout" : 60 // new in 1.9.1 //} },
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Wed Apr 12 18:08:45 2023 +0200 @@ -26,6 +26,7 @@ #include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" #include "../../../OrthancFramework/Sources/FileStorage/IStorageArea.h" #include "../ExportedResource.h" +#include "../Search/ISqlLookupFormatter.h" #include "../ServerIndexChange.h" #include "IDatabaseListener.h" @@ -200,6 +201,8 @@ std::list<std::string>* instancesId, // Can be NULL if not needed const std::vector<DatabaseConstraint>& lookup, ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, uint32_t limit) = 0; // Returns "true" iff. the instance is new and has been inserted @@ -236,6 +239,24 @@ 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; + + // List the labels of one single resource + virtual void ListLabels(std::set<std::string>& target, + int64_t resource) = 0; + + // List all the labels that are present in any resource + virtual void ListAllLabels(std::set<std::string>& target) = 0; }; @@ -260,5 +281,7 @@ IStorageArea& storageArea) = 0; virtual bool HasRevisionsSupport() const = 0; + + virtual bool HasLabelsSupport() const = 0; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallLabelsTable.sql Wed Apr 12 18:08:45 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 <http://www.gnu.org/licenses/>. + + +CREATE TABLE Labels( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + label TEXT NOT NULL, + PRIMARY KEY(id, label) -- Prevents duplicates + ); + +CREATE INDEX LabelsIndex1 ON Labels(id); +CREATE INDEX LabelsIndex2 ON Labels(label); -- This index allows efficient lookups
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Wed Apr 12 18:08:45 2023 +0200 @@ -91,6 +91,13 @@ patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE ); +-- New in Orthanc 1.12.0 +CREATE TABLE Labels( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + label TEXT NOT NULL, + PRIMARY KEY(id, 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(id); +CREATE INDEX LabelsIndex2 ON Labels(label); -- This index allows efficient lookups + CREATE TRIGGER AttachedFileDeleted AFTER DELETE ON AttachedFiles BEGIN
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -341,12 +341,14 @@ std::list<std::string>* instancesId, const std::vector<DatabaseConstraint>& lookup, ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, uint32_t limit) ORTHANC_OVERRIDE { LookupFormatter formatter; std::string sql; - LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit); + LookupFormatter::Apply(sql, formatter, lookup, queryLevel, labels, labelsConstraint, limit); sql = "CREATE TEMPORARY TABLE Lookup AS " + sql; @@ -1071,6 +1073,70 @@ 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 (id, 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 id=? AND label=?"); + s.BindInt64(0, resource); + s.BindString(1, label); + s.Run(); + } + } + + + virtual void ListLabels(std::set<std::string>& target, + int64_t resource) ORTHANC_OVERRIDE + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT label FROM Labels WHERE id=?"); + s.BindInt64(0, resource); + + while (s.Step()) + { + target.insert(s.ColumnString(0)); + } + } + + + virtual void ListAllLabels(std::set<std::string>& target) ORTHANC_OVERRIDE + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT DISTINCT label FROM Labels"); + + while (s.Step()) + { + target.insert(s.ColumnString(0)); + } + } }; @@ -1344,9 +1410,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 +1421,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);
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Wed Apr 12 18:08:45 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
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -607,7 +607,7 @@ Transaction transaction(db_, *factory_, TransactionType_ReadOnly); // TODO - Only if not "TransactionType_Implicit" { - ReadOnlyTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext()); + ReadOnlyTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext(), db_.HasLabelsSupport()); readOperations->Apply(t); } transaction.Commit(); @@ -618,7 +618,7 @@ Transaction transaction(db_, *factory_, TransactionType_ReadWrite); { - ReadWriteTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext()); + ReadWriteTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext(), db_.HasLabelsSupport()); writeOperations->Apply(t); } transaction.Commit(); @@ -715,10 +715,10 @@ const std::string& publicId, ResourceType level, const std::set<DicomTag>& requestedTags, - ExpandResourceDbFlags expandFlags) + ExpandResourceFlags expandFlags) { class Operations : public ReadOnlyOperationsT6< - bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, ExpandResourceDbFlags> + bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, 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,12 @@ } } + if ((expandFlags & ExpandResourceFlags_IncludeLabels) && + transaction.HasLabelsSupport()) + { + transaction.ListLabels(target.labels_, internalId); + } + std::string tmp; if (LookupStringMetadata(tmp, target.metadata_, MetadataType_AnonymizedFrom)) @@ -1066,7 +1071,7 @@ void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target, ResourceType resourceType, size_t since, - size_t limit) + uint32_t limit) { if (limit == 0) { @@ -1646,7 +1651,8 @@ { // TODO - CANDIDATE FOR "TransactionType_Implicit" std::list<std::string> tmp; - transaction.ApplyLookupResources(tmp, NULL, query_, level_, 0); + std::set<std::string> labels; + transaction.ApplyLookupResources(tmp, NULL, query_, level_, labels, LabelsConstraint_Any, 0); CopyListToVector(result_, tmp); } }; @@ -1915,9 +1921,12 @@ std::vector<std::string>* instancesId, const DatabaseLookup& lookup, ResourceType queryLevel, - size_t limit) + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, + uint32_t limit) { - class Operations : public ReadOnlyOperationsT4<bool, const std::vector<DatabaseConstraint>&, ResourceType, size_t> + class Operations : public ReadOnlyOperationsT6<bool, const std::vector<DatabaseConstraint>&, ResourceType, + const std::set<std::string>&, LabelsConstraint, size_t> { private: std::list<std::string> resourcesList_; @@ -1940,21 +1949,33 @@ // 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>()); } } }; + if (!labels.empty() && + !db_.HasLabelsSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels"); + } + + for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + ServerToolbox::CheckValidLabel(*it); + } std::vector<DatabaseConstraint> normalized; NormalizeLookup(normalized, lookup, queryLevel); Operations operations; - operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, limit); + operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, labels, labelsConstraint, limit); CopyListToVector(resourcesId, operations.GetResourcesList()); @@ -3526,4 +3547,117 @@ Apply(operations); return operations.GetStatus(); } + + + void StatelessDatabaseOperations::ListLabels(std::set<std::string>& target, + const std::string& publicId, + ResourceType level) + { + class Operations : public ReadOnlyOperationsT3<std::set<std::string>&, 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::ListAllLabels(std::set<std::string>& target) + { + class Operations : public ReadOnlyOperationsT1<std::set<std::string>& > + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ListAllLabels(tuple.get<0>()); + } + }; + + Operations operations; + operations.Apply(*this, target); + } + + + 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); + } + } + } + }; + + ServerToolbox::CheckValidLabel(label); + + Operations operations(publicId, level, label, operation); + Apply(operations); + } + + + bool StatelessDatabaseOperations::HasLabelsSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.HasLabelsSupport(); + } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed Apr 12 18:08:45 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<std::string> childrenIds_; std::map<MetadataType, std::string> 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<std::string> 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<FileInfo> Attachments; typedef std::map<std::pair<ResourceType, MetadataType>, std::string> MetadataMap; + enum LabelOperation + { + LabelOperation_Add, + LabelOperation_Remove + }; + class ITransactionContext : public IDatabaseListener { public: @@ -124,14 +166,17 @@ { private: ITransactionContext& context_; + bool hasLabelsSupport_; protected: IDatabaseWrapper::ITransaction& transaction_; public: explicit ReadOnlyTransaction(IDatabaseWrapper::ITransaction& transaction, - ITransactionContext& context) : + ITransactionContext& context, + bool hasLabelsSupport) : context_(context), + hasLabelsSupport_(hasLabelsSupport), transaction_(transaction) { } @@ -141,6 +186,11 @@ return context_; } + bool HasLabelsSupport() const + { + return hasLabelsSupport_; + } + /** * Higher-level constructions **/ @@ -157,9 +207,12 @@ std::list<std::string>* instancesId, // Can be NULL if not needed const std::vector<DatabaseConstraint>& lookup, ResourceType queryLevel, - size_t limit) + const std::set<std::string>& labels, // New in Orthanc 1.12.0 + LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 + uint32_t limit) { - return transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit); + return transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, + labels, labelsConstraint, limit); } void GetAllMetadata(std::map<MetadataType, std::string>& target, @@ -177,7 +230,7 @@ void GetAllPublicIds(std::list<std::string>& target, ResourceType resourceType, size_t since, - size_t limit) + uint32_t limit) { return transaction_.GetAllPublicIds(target, resourceType, since, limit); } @@ -185,9 +238,9 @@ void GetChanges(std::list<ServerIndexChange>& 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<int64_t>& target, @@ -205,9 +258,9 @@ void GetExportedResources(std::list<ExportedResource>& 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<ServerIndexChange>& target /*out*/) @@ -310,6 +363,17 @@ { return transaction_.LookupResourceAndParent(id, type, parentPublicId, publicId); } + + void ListLabels(std::set<std::string>& target, + int64_t id) + { + transaction_.ListLabels(target, id); + } + + void ListAllLabels(std::set<std::string>& target) + { + transaction_.ListAllLabels(target); + } }; @@ -317,8 +381,9 @@ { public: ReadWriteTransaction(IDatabaseWrapper::ITransaction& transaction, - ITransactionContext& context) : - ReadOnlyTransaction(transaction, context) + ITransactionContext& context, + bool hasLabelsSupport) : + ReadOnlyTransaction(transaction, context, hasLabelsSupport) { } @@ -422,6 +487,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 +580,7 @@ const std::string& publicId, ResourceType level, const std::set<DicomTag>& requestedTags, - ExpandResourceDbFlags expandFlags); + ExpandResourceFlags expandFlags); void GetAllMetadata(std::map<MetadataType, std::string>& target, const std::string& publicId, @@ -515,7 +592,7 @@ void GetAllUuids(std::list<std::string>& target, ResourceType resourceType, size_t since, - size_t limit); + uint32_t limit); void GetGlobalStatistics(/* out */ uint64_t& diskSize, /* out */ uint64_t& uncompressedSize, @@ -531,13 +608,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 +682,9 @@ std::vector<std::string>* instancesId, // Can be NULL if not needed const DatabaseLookup& lookup, ResourceType queryLevel, - size_t limit); + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, + uint32_t limit); bool DeleteResource(Json::Value& remainingAncestor /* out */, const std::string& uuid, @@ -683,5 +762,18 @@ bool hasOldRevision, int64_t oldRevision, const std::string& oldMd5); + + void ListLabels(std::set<std::string>& target, + const std::string& publicId, + ResourceType level); + + void ListAllLabels(std::set<std::string>& target); + + void ModifyLabel(const std::string& publicId, + ResourceType level, + const std::string& label, + LabelOperation operation); + + bool HasLabelsSupport(); }; }
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Wed Apr 12 18:08:45 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() &&
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -1431,7 +1431,7 @@ "Host that is used for this commands, defaults to `Host` configuration option. " "Allows you to overwrite the destination host for a specific operation.", false) .SetRequestField(KEY_PORT, RestApiCallDocumentation::Type_Number, - "Port that is used for this commands, defaults to `Port` configuration option. " + "Port that is used for this command, defaults to `Port` configuration option. " "Allows you to overwrite the destination port for a specific operation.", false) .SetRequestField(KEY_MOVE_ORIGINATOR_AET, RestApiCallDocumentation::Type_String, "Move originator AET that is used for this commands, in order to fake a C-MOVE SCU", false)
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Apr 12 18:08:45 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<std::string> labels; + OrthancRestApi::GetIndex(call).ListLabels(labels, publicId, level); + + Json::Value result = Json::arrayValue; + + for (std::set<std::string>::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<std::string> 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_LABELS = "Labels"; // New in Orthanc 1.12.0 + static const char* const KEY_LABELS_CONSTRAINT = "LabelsConstraint"; // 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_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) .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_LABELS) && + request[KEY_LABELS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LABELS) + "\" must be an array of strings"); + } + else if (request.isMember(KEY_LABELS_CONSTRAINT) && + request[KEY_LABELS_CONSTRAINT].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" 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<size_t>(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<size_t>(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,44 @@ } } + 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 + { + query.AddLabel(request[KEY_LABELS][i].asString()); + } + } + } + + query.SetLabelsConstraint(LabelsConstraint_All); + + if (request.isMember(KEY_LABELS_CONSTRAINT)) + { + const std::string& s = request[KEY_LABELS_CONSTRAINT].asString(); + if (s == "All") + { + query.SetLabelsConstraint(LabelsConstraint_All); + } + else if (s == "Any") + { + query.SetLabelsConstraint(LabelsConstraint_Any); + } + else if (s == "None") + { + query.SetLabelsConstraint(LabelsConstraint_None); + } + else + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\""); + } + } + FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human), context.GetFindStorageAccessMode()); context.Apply(visitor, query, level, since, limit); visitor.Answer(call.GetOutput(), context, level, expand, requestedTags); @@ -3854,6 +4033,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);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -89,6 +89,7 @@ static const char* const MAXIMUM_STORAGE_SIZE = "MaximumStorageSize"; static const char* const MAXIMUM_STORAGE_MODE = "MaximumStorageMode"; static const char* const USER_METADATA = "UserMetadata"; + static const char* const HAS_LABELS = "HasLabels"; if (call.IsDocumentation()) { @@ -131,6 +132,8 @@ "The configured MaximumStorageMode (new in Orthanc 1.11.3)") .SetAnswerField(USER_METADATA, RestApiCallDocumentation::Type_JsonObject, "The configured UserMetadata (new in Orthanc 1.12.0)") + .SetAnswerField(HAS_LABELS, RestApiCallDocumentation::Type_Boolean, + "Whether the database back-end supports labels (new in Orthanc 1.12.0)") .SetHttpGetSample("https://demo.orthanc-server.com/system", true); return; } @@ -187,6 +190,8 @@ result[USER_METADATA] = Json::objectValue; GetUserMetadataConfiguration(result[USER_METADATA]); + result[HAS_LABELS] = OrthancRestApi::GetIndex(call).HasLabelsSupport(); + call.GetOutput().AnswerJson(result); } @@ -1042,6 +1047,31 @@ } + static void ListAllLabels(RestApiGetCall& call) + { + if (call.IsDocumentation()) + { + call.GetDocumentation() + .SetTag("System") + .SetSummary("Get all the used labels") + .SetDescription("List all the labels that are associated with any resource of the Orthanc database") + .AddAnswerType(MimeType_Json, "JSON array containing the labels"); + return; + } + + std::set<std::string> labels; + OrthancRestApi::GetIndex(call).ListAllLabels(labels); + + Json::Value json = Json::arrayValue; + for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + json.append(*it); + } + + call.GetOutput().AnswerJson(json); + } + + void OrthancRestApi::RegisterSystem(bool orthancExplorerEnabled) { if (orthancExplorerEnabled) @@ -1089,5 +1119,7 @@ Register("/tools/accepted-transfer-syntaxes", SetAcceptedTransferSyntaxes); Register("/tools/unknown-sop-class-accepted", GetUnknownSopClassAccepted); Register("/tools/unknown-sop-class-accepted", SetUnknownSopClassAccepted); + + Register("/tools/labels", ListAllLabels); // New in Orthanc 1.12.0 } }
--- a/OrthancServer/Sources/Search/DatabaseLookup.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -368,4 +368,14 @@ return clone.release(); } + + + void DatabaseLookup::AddLabel(const std::string& label) + { + if (!label.empty()) + { + ServerToolbox::CheckValidLabel(label); + labels_.insert(label); + } + } }
--- a/OrthancServer/Sources/Search/DatabaseLookup.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.h Wed Apr 12 18:08:45 2023 +0200 @@ -22,6 +22,7 @@ #pragma once +#include "../Search/ISqlLookupFormatter.h" #include "DicomTagConstraint.h" class DcmItem; @@ -32,6 +33,8 @@ { private: std::vector<DicomTagConstraint*> constraints_; + std::set<std::string> labels_; + LabelsConstraint labelsConstraint_; void AddDicomConstraintInternal(const DicomTag& tag, ValueRepresentation vr, @@ -42,7 +45,8 @@ void AddConstraintInternal(DicomTagConstraint* constraint); // Takes ownership public: - DatabaseLookup() + DatabaseLookup() : + labelsConstraint_(LabelsConstraint_All) { } @@ -92,5 +96,22 @@ bool HasTag(const DicomTag& tag) const; void RemoveConstraint(const DicomTag& tag); + + void AddLabel(const std::string& label); + + void SetLabelsConstraint(LabelsConstraint constraint) + { + labelsConstraint_ = constraint; + } + + const std::set<std::string>& GetLabels() const + { + return labels_; + } + + LabelsConstraint GetLabelsConstraint() const + { + return labelsConstraint_; + } }; }
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -39,6 +39,7 @@ #include "DatabaseConstraint.h" #include <boost/lexical_cast.hpp> +#include <list> namespace Orthanc @@ -268,12 +269,46 @@ " AND " + tag + ".tagElement = " + boost::lexical_cast<std::string>(constraint.GetTag().GetElement())); } + + + static std::string Join(const std::list<std::string>& values, + const std::string& prefix, + const std::string& separator) + { + if (values.empty()) + { + return ""; + } + else + { + std::string s = prefix; + + bool first = true; + for (std::list<std::string>::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<DatabaseConstraint>& lookup, ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, size_t limit) { assert(ResourceType_Patient < ResourceType_Study && @@ -346,9 +381,50 @@ FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" + FormatLevel(static_cast<ResourceType>(level)) + ".parentId"); } + + std::list<std::string> where; + where.push_back(FormatLevel(queryLevel) + ".resourceType = " + + formatter.FormatResourceType(queryLevel) + comparisons); + + if (!labels.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<std::string> formattedLabels; + for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + formattedLabels.push_back(formatter.GenerateParameter(*it)); + } + + std::string condition; + switch (labelsConstraint) + { + case LabelsConstraint_Any: + condition = "> 0"; + break; + + case LabelsConstraint_All: + condition = "= " + boost::lexical_cast<std::string>(labels.size()); + break; + + case LabelsConstraint_None: + condition = "= 0"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } - sql += (joins + " WHERE " + FormatLevel(queryLevel) + ".resourceType = " + - formatter.FormatResourceType(queryLevel) + comparisons); + where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + FormatLevel(queryLevel) + + ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition); + } + + sql += joins + Join(where, " WHERE ", " AND "); if (limit != 0) {
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h Wed Apr 12 18:08:45 2023 +0200 @@ -35,6 +35,13 @@ { class DatabaseConstraint; + enum LabelsConstraint + { + LabelsConstraint_All, + LabelsConstraint_Any, + LabelsConstraint_None + }; + // This class is also used by the "orthanc-databases" project class ISqlLookupFormatter : public boost::noncopyable { @@ -60,6 +67,8 @@ ISqlLookupFormatter& formatter, const std::vector<DatabaseConstraint>& lookup, ResourceType queryLevel, + const std::set<std::string>& labels, // New in Orthanc 1.12.0 + LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 size_t limit); }; }
--- a/OrthancServer/Sources/ServerContext.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Apr 12 18:08:45 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.GetLabels(), lookup.GetLabelsConstraint(), lookupLimit); } bool complete = (databaseLimit == 0 || @@ -1542,7 +1543,7 @@ ComputeStudyTags(resource, *this, resources[i], requestedTags); std::vector<std::string> 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<std::string>::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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<std::string>(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<DicomTag>& 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<DicomTag> retrievedTags; std::set<DicomTag> 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<ExpandResourceDbFlags>(expandFlags & ~ExpandResourceDbFlags_IncludeMainDicomTags); + expandFlags = static_cast<ExpandResourceFlags>(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<ExpandResourceDbFlags>(expandFlags | ExpandResourceDbFlags_IncludeMetadata))) // we always need the metadata to get the mainDicomTagsSignature + if (expandFlags != ExpandResourceFlags_None && + GetIndex().ExpandResource(resource, publicId, level, requestedTags, + static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeMetadata))) // we always need the metadata to get the mainDicomTagsSignature { // check the main dicom tags list has not changed since the resource was stored - if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.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
--- a/OrthancServer/Sources/ServerContext.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/ServerContext.h Wed Apr 12 18:08:45 2023 +0200 @@ -585,7 +585,7 @@ const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource ResourceType level, const std::set<DicomTag>& requestedTags, - ExpandResourceDbFlags expandFlags, + ExpandResourceFlags expandFlags, bool allowStorageAccess); FindStorageAccessMode GetFindStorageAccessMode() const
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Wed Apr 12 18:08:45 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>(ExpandResourceDbFlags_IncludeMainDicomTags | ExpandResourceDbFlags_IncludeChildren))) + if (GetContext().GetIndex().ExpandResource(targetPatient, lookupPatientResult[0], ResourceType_Patient, emptyRequestedTags, static_cast<ExpandResourceFlags>(ExpandResourceFlags_IncludeMainDicomTags | ExpandResourceFlags_IncludeChildren))) { const std::list<std::string> 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<DicomTag> mainPatientTags; DicomMap::GetMainDicomTags(mainPatientTags, ResourceType_Patient); @@ -755,9 +758,9 @@ for (std::set<DicomTag>::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 @@ } } } - } - + } } } }
--- a/OrthancServer/Sources/ServerToolbox.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -287,5 +287,47 @@ } } } + + + bool IsValidLabel(const std::string& label) + { + if (label.empty()) + { + return false; + } + + if (label.size() > 64) + { + // This limitation is for MySQL, which cannot use a TEXT + // column of undefined length as a primary key + return false; + } + + for (size_t i = 0; i < label.size(); i++) + { + if (!(label[i] == '_' || + label[i] == '-' || + (label[i] >= 'a' && label[i] <= 'z') || + (label[i] >= 'A' && label[i] <= 'Z') || + (label[i] >= '0' && label[i] <= '9'))) + { + return false; + } + } + + return true; + } + + + void CheckValidLabel(const std::string& label) + { + if (!IsValidLabel(label)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "A label must be a non-empty, alphanumeric string, " + "possibly with '_' or '-' characters, " + "with maximum 64 characters, but got: " + label); + } + } } }
--- a/OrthancServer/Sources/ServerToolbox.h Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/ServerToolbox.h Wed Apr 12 18:08:45 2023 +0200 @@ -56,5 +56,9 @@ void ReconstructResource(ServerContext& context, const std::string& resource, bool reconstructFiles); + + bool IsValidLabel(const std::string& label); + + void CheckValidLabel(const std::string& label); } }
--- a/OrthancServer/Sources/main.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/Sources/main.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -1664,8 +1664,12 @@ } } - bool success = ConfigureServerContext - (database, storageArea, plugins, loadJobsFromDatabase); + if (!database.HasLabelsSupport()) + { + LOG(WARNING) << "The custom database back-end has *no* support for labels"; + } + + bool success = ConfigureServerContext(database, storageArea, plugins, loadJobsFromDatabase); database.Close();
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Wed Apr 12 18:04:05 2023 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Wed Apr 12 18:08:45 2023 +0200 @@ -167,8 +167,9 @@ std::vector<DatabaseConstraint> lookup; lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); - - transaction_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */); + + std::set<std::string> noLabel; + transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */); } void DoLookupIdentifier2(std::list<std::string>& 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<std::string> noLabel; + transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */); } }; } @@ -1038,3 +1040,15 @@ } } } + + +TEST(ServerToolbox, ValidLabels) +{ + ASSERT_TRUE(ServerToolbox::IsValidLabel("abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + ASSERT_TRUE(ServerToolbox::IsValidLabel("0123456789-_")); + ASSERT_FALSE(ServerToolbox::IsValidLabel("")); + ASSERT_FALSE(ServerToolbox::IsValidLabel(" ")); + ASSERT_FALSE(ServerToolbox::IsValidLabel("&")); + ASSERT_FALSE(ServerToolbox::IsValidLabel(".")); +}