Mercurial > hg > orthanc
changeset 1383:5c11c4e728eb query-retrieve
integration mainline->query-retrieve
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 29 May 2015 14:46:55 +0200 |
parents | 21a2929e541d (diff) 1cd2e09cb0e5 (current diff) |
children | 772c8507c68d |
files | NEWS OrthancServer/OrthancFindRequestHandler.cpp OrthancServer/OrthancRestApi/OrthancRestResources.cpp Resources/Configuration.json |
diffstat | 34 files changed, 1633 insertions(+), 245 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Fri May 29 14:46:06 2015 +0200 +++ b/CMakeLists.txt Fri May 29 14:46:55 2015 +0200 @@ -70,6 +70,7 @@ set(ORTHANC_CORE_SOURCES Core/Cache/MemoryCache.cpp + Core/Cache/SharedArchive.cpp Core/ChunkedBuffer.cpp Core/Compression/BufferCompressor.cpp Core/Compression/ZlibCompressor.cpp @@ -172,6 +173,7 @@ OrthancServer/ExportedResource.cpp OrthancServer/ResourceFinder.cpp OrthancServer/DicomFindQuery.cpp + OrthancServer/QueryRetrieveHandler.cpp # From "lua-scripting" branch OrthancServer/DicomInstanceToStore.cpp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/SharedArchive.cpp Fri May 29 14:46:55 2015 +0200 @@ -0,0 +1,134 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeaders.h" +#include "SharedArchive.h" + + +#include "../Uuid.h" + + +namespace Orthanc +{ + void SharedArchive::RemoveInternal(const std::string& id) + { + Archive::iterator it = archive_.find(id); + + if (it != archive_.end()) + { + delete it->second; + archive_.erase(it); + } + } + + + SharedArchive::Accessor::Accessor(SharedArchive& that, + const std::string& id) : + lock_(that.mutex_) + { + Archive::iterator it = that.archive_.find(id); + + if (it == that.archive_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + that.lru_.MakeMostRecent(id); + item_ = it->second; + } + } + + + SharedArchive::SharedArchive(size_t maxSize) : + maxSize_(maxSize) + { + if (maxSize == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + SharedArchive::~SharedArchive() + { + for (Archive::iterator it = archive_.begin(); + it != archive_.end(); it++) + { + delete it->second; + } + } + + + std::string SharedArchive::Add(IDynamicObject* obj) + { + boost::mutex::scoped_lock lock(mutex_); + + if (archive_.size() == maxSize_) + { + // The quota has been reached, remove the oldest element + std::string oldest = lru_.RemoveOldest(); + RemoveInternal(oldest); + } + + std::string id = Toolbox::GenerateUuid(); + RemoveInternal(id); // Should never be useful because of UUID + archive_[id] = obj; + lru_.Add(id); + + return id; + } + + + void SharedArchive::Remove(const std::string& id) + { + boost::mutex::scoped_lock lock(mutex_); + RemoveInternal(id); + lru_.Invalidate(id); + } + + + void SharedArchive::List(std::list<std::string>& items) + { + items.clear(); + + boost::mutex::scoped_lock lock(mutex_); + + for (Archive::const_iterator it = archive_.begin(); + it != archive_.end(); it++) + { + items.push_back(it->first); + } + } +} + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/SharedArchive.h Fri May 29 14:46:55 2015 +0200 @@ -0,0 +1,85 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "LeastRecentlyUsedIndex.h" +#include "../IDynamicObject.h" + +#include <map> +#include <boost/thread.hpp> + +namespace Orthanc +{ + class SharedArchive : public boost::noncopyable + { + private: + typedef std::map<std::string, IDynamicObject*> Archive; + + size_t maxSize_; + boost::mutex mutex_; + Archive archive_; + Orthanc::LeastRecentlyUsedIndex<std::string> lru_; + + void RemoveInternal(const std::string& id); + + public: + class Accessor : public boost::noncopyable + { + private: + boost::mutex::scoped_lock lock_; + IDynamicObject* item_; + + public: + Accessor(SharedArchive& that, + const std::string& id); + + IDynamicObject& GetItem() const + { + return *item_; + } + }; + + + SharedArchive(size_t maxSize); + + ~SharedArchive(); + + std::string Add(IDynamicObject* obj); // Takes the ownership + + void Remove(const std::string& id); + + void List(std::list<std::string>& items); + }; +} + +
--- a/Core/DicomFormat/DicomMap.cpp Fri May 29 14:46:06 2015 +0200 +++ b/Core/DicomFormat/DicomMap.cpp Fri May 29 14:46:55 2015 +0200 @@ -273,6 +273,16 @@ result.SetValue(DICOM_TAG_ACCESSION_NUMBER, ""); result.SetValue(DICOM_TAG_PATIENT_ID, ""); result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, ""); + + // These tags are considered as "main" by Orthanc, but are not in the Series module + result.Remove(DicomTag(0x0008, 0x0070)); // Manufacturer + result.Remove(DicomTag(0x0008, 0x1010)); // Station name + result.Remove(DicomTag(0x0018, 0x0024)); // Sequence name + result.Remove(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES); + result.Remove(DICOM_TAG_IMAGES_IN_ACQUISITION); + result.Remove(DICOM_TAG_NUMBER_OF_SLICES); + result.Remove(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS); + result.Remove(DICOM_TAG_NUMBER_OF_TIME_SLICES); } void DicomMap::SetupFindInstanceTemplate(DicomMap& result)
--- a/Core/DicomFormat/DicomTag.cpp Fri May 29 14:46:06 2015 +0200 +++ b/Core/DicomFormat/DicomTag.cpp Fri May 29 14:46:55 2015 +0200 @@ -118,11 +118,10 @@ } - void DicomTag::GetTagsForModule(std::set<DicomTag>& target, + void DicomTag::AddTagsForModule(std::set<DicomTag>& target, DicomModule module) { // REFERENCE: 11_03pu.pdf, DICOM PS 3.3 2011 - Information Object Definitions - target.clear(); switch (module) {
--- a/Core/DicomFormat/DicomTag.h Fri May 29 14:46:06 2015 +0200 +++ b/Core/DicomFormat/DicomTag.h Fri May 29 14:46:55 2015 +0200 @@ -84,7 +84,7 @@ friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag); - static void GetTagsForModule(std::set<DicomTag>& target, + static void AddTagsForModule(std::set<DicomTag>& target, DicomModule module); bool IsIdentifier() const;
--- a/Core/RestApi/RestApi.cpp Fri May 29 14:46:06 2015 +0200 +++ b/Core/RestApi/RestApi.cpp Fri May 29 14:46:55 2015 +0200 @@ -163,7 +163,7 @@ const GetArguments& getArguments, const std::string& postData) { - RestApiOutput wrappedOutput(output); + RestApiOutput wrappedOutput(output, method); #if ORTHANC_PUGIXML_ENABLED == 1 // Look if the user wishes XML answers instead of JSON
--- a/Core/RestApi/RestApiCall.cpp Fri May 29 14:46:06 2015 +0200 +++ b/Core/RestApi/RestApiCall.cpp Fri May 29 14:46:55 2015 +0200 @@ -41,4 +41,17 @@ Json::Reader reader; return reader.parse(request, result); } + + + std::string RestApiCall::FlattenUri() const + { + std::string s = "/"; + + for (size_t i = 0; i < fullUri_.size(); i++) + { + s += fullUri_[i] + "/"; + } + + return s; + } }
--- a/Core/RestApi/RestApiCall.h Fri May 29 14:46:06 2015 +0200 +++ b/Core/RestApi/RestApiCall.h Fri May 29 14:46:55 2015 +0200 @@ -114,6 +114,8 @@ HttpHandler::ParseCookies(result, httpHeaders_); } + std::string FlattenUri() const; + virtual bool ParseJsonRequest(Json::Value& result) const = 0; }; }
--- a/Core/RestApi/RestApiOutput.cpp Fri May 29 14:46:06 2015 +0200 +++ b/Core/RestApi/RestApiOutput.cpp Fri May 29 14:46:55 2015 +0200 @@ -40,8 +40,10 @@ namespace Orthanc { - RestApiOutput::RestApiOutput(HttpOutput& output) : + RestApiOutput::RestApiOutput(HttpOutput& output, + HttpMethod method) : output_(output), + method_(method), convertJsonToXml_(false) { alreadySent_ = false; @@ -55,7 +57,14 @@ { if (!alreadySent_) { - output_.SendStatus(HttpStatus_404_NotFound); + if (method_ == HttpMethod_Post) + { + output_.SendStatus(HttpStatus_400_BadRequest); + } + else + { + output_.SendStatus(HttpStatus_404_NotFound); + } } }
--- a/Core/RestApi/RestApiOutput.h Fri May 29 14:46:06 2015 +0200 +++ b/Core/RestApi/RestApiOutput.h Fri May 29 14:46:55 2015 +0200 @@ -43,13 +43,15 @@ { private: HttpOutput& output_; - bool alreadySent_; - bool convertJsonToXml_; + HttpMethod method_; + bool alreadySent_; + bool convertJsonToXml_; void CheckStatus(); public: - RestApiOutput(HttpOutput& output); + RestApiOutput(HttpOutput& output, + HttpMethod method); ~RestApiOutput();
--- a/NEWS Fri May 29 14:46:06 2015 +0200 +++ b/NEWS Fri May 29 14:46:55 2015 +0200 @@ -4,6 +4,8 @@ Major ----- +* DICOM Query/Retrieve available from Orthanc Explorer +* C-Move SCU and C-Find SCU are accessible through the REST API * "?expand" flag for URIs "/patients", "/studies" and "/series" * "/tools/find" URI to search for DICOM resources from REST * Support of FreeBSD
--- a/OrthancExplorer/explorer.html Fri May 29 14:46:06 2015 +0200 +++ b/OrthancExplorer/explorer.html Fri May 29 14:46:55 2015 +0200 @@ -30,6 +30,7 @@ <link rel="stylesheet" href="explorer.css" /> <script src="file-upload.js"></script> <script src="explorer.js"></script> + <script src="query-retrieve.js"></script> <script src="../plugins/explorer.js"></script> </head> <body> @@ -37,7 +38,10 @@ <div data-role="header" > <h1><span class="orthanc-name"></span>Find a patient</h1> <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <ul id="all-patients" data-role="listview" data-inset="true" data-filter="true"> @@ -75,7 +79,10 @@ <div data-role="header" > <h1><span class="orthanc-name"></span>Patient</h1> <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <div class="ui-grid-a"> @@ -129,7 +136,10 @@ Study </h1> <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <div class="ui-grid-a"> @@ -178,7 +188,10 @@ </h1> <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <div class="ui-grid-a"> @@ -228,7 +241,10 @@ Instance </h1> <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> - <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> + <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> + <a href="#upload" data-icon="gear" data-role="button">Upload</a> + <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a> + </div> </div> <div data-role="content"> <div class="ui-grid-a"> @@ -284,6 +300,90 @@ </div> </div> + <div data-role="page" id="query-retrieve" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>DICOM Query/Retrieve (1/3)</h1> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> + </div> + <div data-role="content"> + <form data-ajax="false"> + <div data-role="fieldcontain"> + <label for="qr-server">DICOM server:</label> + <select name="qr-server" id="qr-server"> + </select> + </div> + + <div data-role="fieldcontain" id="qr-fields"> + <fieldset data-role="controlgroup"> + <legend>Field of interest:</legend> + <input type="radio" name="qr-field" id="qr-patient-id" value="PatientID" checked="checked" /> + <label for="qr-patient-id">Patient ID</label> + <input type="radio" name="qr-field" id="qr-patient-name" value="PatientName" /> + <label for="qr-patient-name">Patient Name</label> + <input type="radio" name="qr-field" id="qr-accession-number" value="AccessionNumber" /> + <label for="qr-accession-number">Accession Number</label> + <input type="radio" name="qr-field" id="qr-study-description" value="StudyDescription" /> + <label for="qr-study-description">Study Description</label> + </fieldset> + </div> + + <div data-role="fieldcontain"> + <label for="qr-value">Value for this field:</label> + <input type="text" name="qr-value" id="qr-value" value="*" /> + </div> + + <div data-role="fieldcontain"> + <label for="qr-date">Study date:</label> + <select name="qr-date" id="qr-date"> + </select> + </div> + + <div data-role="fieldcontain" id="qr-modalities"> + <div data-role="fieldcontain"> + <fieldset data-role="controlgroup" data-type="horizontal"> + <legend>Modalities:</legend> + <input type="checkbox" name="CR" id="qr-cr" class="custom" /> <label for="qr-cr">CR</label> + <input type="checkbox" name="CT" id="qr-ct" class="custom" /> <label for="qr-ct">CT</label> + <input type="checkbox" name="MR" id="qr-mr" class="custom" /> <label for="qr-mr">MR</label> + <input type="checkbox" name="NM" id="qr-nm" class="custom" /> <label for="qr-nm">NM</label> + <input type="checkbox" name="PT" id="qr-pt" class="custom" /> <label for="qr-pt">PT</label> + <input type="checkbox" name="US" id="qr-us" class="custom" /> <label for="qr-us">US</label> + <input type="checkbox" name="XA" id="qr-xa" class="custom" /> <label for="qr-xa">XA</label> + </fieldset> + </div> + </div> + + <button id="qr-submit" type="submit" data-theme="b">Search studies</button> + </form> + </div> + </div> + + + <div data-role="page" id="query-retrieve-2" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>DICOM Query/Retrieve (2/3)</h1> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> + <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a> + </div> + <div data-role="content"> + <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b"> + </ul> + </div> + </div> + + + <div data-role="page" id="query-retrieve-3" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>DICOM Query/Retrieve (3/3)</h1> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> + <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a> + </div> + <div data-role="content"> + <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b"> + </ul> + </div> + </div> + <div id="peer-store" style="display:none;" class="ui-body-c"> <p align="center"><b>Sending to Orthanc peer...</b></p> @@ -295,6 +395,11 @@ <p><img src="libs/images/ajax-loader2.gif" alt="" /></p> </div> + <div id="info-retrieve" style="display:none;" class="ui-body-c"> + <p align="center"><b>Retrieving images from DICOM modality...</b></p> + <p><img src="libs/images/ajax-loader2.gif" alt="" /></p> + </div> + <div id="dialog" style="display:none" > </div> </body>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancExplorer/query-retrieve.js Fri May 29 14:46:55 2015 +0200 @@ -0,0 +1,265 @@ +function JavascriptDateToDicom(date) +{ + var s = date.toISOString(); + return s.substring(0, 4) + s.substring(5, 7) + s.substring(8, 10); +} + +function GenerateDicomDate(days) +{ + var today = new Date(); + var other = new Date(today); + other.setDate(today.getDate() + days); + return JavascriptDateToDicom(other); +} + + +$('#query-retrieve').live('pagebeforeshow', function() { + $.ajax({ + url: '../modalities', + dataType: 'json', + async: false, + cache: false, + success: function(modalities) { + var target = $('#qr-server'); + $('option', target).remove(); + + for (var i = 0; i < modalities.length; i++) { + var option = $('<option>').text(modalities[i]); + target.append(option); + } + + target.selectmenu('refresh'); + } + }); + + var target = $('#qr-date'); + $('option', target).remove(); + target.append($('<option>').attr('value', '*').text('Any date')); + target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today')); + target.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday')); + target.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days')); + target.append($('<option>').attr('value', GenerateDicomDate(-31) + '-').text('Last 31 days')); + target.append($('<option>').attr('value', GenerateDicomDate(-31 * 3) + '-').text('Last 3 months')); + target.append($('<option>').attr('value', GenerateDicomDate(-365) + '-').text('Last year')); + target.selectmenu('refresh'); +}); + + +$('#qr-submit').live('click', function() { + var query = { + 'Level' : 'Study', + 'Query' : { + 'AccessionNumber' : '*', + 'PatientBirthDate' : '*', + 'PatientID' : '*', + 'PatientName' : '*', + 'PatientSex' : '*', + 'SpecificCharacterSet' : 'ISO_IR 192', // UTF-8 + 'StudyDate' : $('#qr-date').val(), + 'StudyDescription' : '*' + } + }; + + var field = $('#qr-fields input:checked').val(); + query['Query'][field] = $('#qr-value').val().toUpperCase(); + + var modalities = ''; + $('#qr-modalities input:checked').each(function() { + var s = $(this).attr('name'); + if (modalities == '*') + modalities = s; + else + modalities += '\\' + s; + }); + + if (modalities.length > 0) { + query['Query']['ModalitiesInStudy'] = modalities; + } + + + var server = $('#qr-server').val(); + $.ajax({ + url: '../modalities/' + server + '/query', + type: 'POST', + data: JSON.stringify(query), + dataType: 'json', + async: false, + error: function() { + alert('Error during query (C-Find)'); + }, + success: function(result) { + window.location.assign('explorer.html#query-retrieve-2?server=' + server + '&uuid=' + result['ID']); + } + }); + + return false; +}); + + + +function Retrieve(url) +{ + $.ajax({ + url: '../system', + dataType: 'json', + async: false, + success: function(system) { + $('<div>').simpledialog2({ + mode: 'button', + headerText: 'Target', + headerClose: true, + buttonPrompt: 'Enter Application Entity Title (AET):', + buttonInput: true, + buttonInputDefault: system['DicomAet'], + buttons : { + 'OK': { + click: function () { + var aet = $.mobile.sdLastInput; + if (aet.length == 0) + aet = system['DicomAet']; + + $.ajax({ + url: url, + type: 'POST', + async: true, // Necessary to block UI + dataType: 'text', + data: aet, + beforeSend: function() { + $.blockUI({ message: $('#info-retrieve') }); + }, + complete: function(s) { + $.unblockUI(); + }, + error: function() { + alert('Error during retrieve'); + } + }); + } + } + } + }); + } + }); +} + + + + +$('#query-retrieve-2').live('pagebeforeshow', function() { + if ($.mobile.pageData) { + var uri = '../queries/' + $.mobile.pageData.uuid + '/answers'; + + $.ajax({ + url: uri, + dataType: 'json', + async: false, + success: function(answers) { + var target = $('#query-retrieve-2 ul'); + $('li', target).remove(); + + for (var i = 0; i < answers.length; i++) { + $.ajax({ + url: uri + '/' + answers[i] + '/content?simplify', + dataType: 'json', + async: false, + success: function(study) { + var series = '#query-retrieve-3?server=' + $.mobile.pageData.server + '&uuid=' + study['StudyInstanceUID']; + var info = $('<a>').attr('href', series).html( + ('<h3>{0} - {1}</h3>' + + '<p>Accession number: <b>{2}</b></p>' + + '<p>Birth date: <b>{3}</b></p>' + + '<p>Patient sex: <b>{4}</b></p>' + + '<p>Study description: <b>{5}</b></p>' + + '<p>Study date: <b>{6}</b></p>').format( + study['PatientID'], + study['PatientName'], + study['AccessionNumber'], + FormatDicomDate(study['PatientBirthDate']), + study['PatientSex'], + study['StudyDescription'], + FormatDicomDate(study['StudyDate']))); + + var studyUri = uri + '/' + answers[i] + '/retrieve'; + var retrieve = $('<a>').text('Retrieve').click(function() { + Retrieve(studyUri); + }); + + target.append($('<li>').append(info).append(retrieve)); + } + }); + } + + target.listview('refresh'); + } + }); + } +}); + + +$('#query-retrieve-3').live('pagebeforeshow', function() { + if ($.mobile.pageData) { + var query = { + 'Level' : 'Series', + 'Query' : { + 'Modality' : '*', + 'ProtocolName' : '*', + 'SeriesDescription' : '*', + 'SeriesInstanceUID' : '*', + 'StudyInstanceUID' : $.mobile.pageData.uuid + } + }; + + $.ajax({ + url: '../modalities/' + $.mobile.pageData.server + '/query', + type: 'POST', + data: JSON.stringify(query), + dataType: 'json', + async: false, + error: function() { + alert('Error during query (C-Find)'); + }, + success: function(answer) { + var uri = '../queries/' + answer['ID'] + '/answers'; + + $.ajax({ + url: uri, + dataType: 'json', + async: false, + success: function(answers) { + + var target = $('#query-retrieve-3 ul'); + $('li', target).remove(); + + for (var i = 0; i < answers.length; i++) { + $.ajax({ + url: uri + '/' + answers[i] + '/content?simplify', + dataType: 'json', + async: false, + success: function(series) { + var info = $('<a>').html( + ('<h3>{0}</h3>' + + '<p>Modality: <b>{1}</b></p>' + + '<p>Protocol name: <b>{2}</b></p>' + ).format( + series['SeriesDescription'], + series['Modality'], + series['ProtocolName'] + )); + + var seriesUri = uri + '/' + answers[i] + '/retrieve'; + var retrieve = $('<a>').text('Retrieve').click(function() { + Retrieve(seriesUri); + }); + + target.append($('<li>').append(info).append(retrieve)); + } + }); + } + + target.listview('refresh'); + } + }); + } + }); + } +});
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Fri May 29 14:46:55 2015 +0200 @@ -53,14 +53,15 @@ } } - void DicomFindAnswers::ToJson(Json::Value& target) const + void DicomFindAnswers::ToJson(Json::Value& target, + bool simplify) const { target = Json::arrayValue; for (size_t i = 0; i < GetSize(); i++) { Json::Value answer(Json::objectValue); - FromDcmtkBridge::ToJson(answer, GetAnswer(i)); + FromDcmtkBridge::ToJson(answer, GetAnswer(i), simplify); target.append(answer); } }
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.h Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h Fri May 29 14:46:55 2015 +0200 @@ -69,6 +69,7 @@ return *items_.at(index); } - void ToJson(Json::Value& target) const; + void ToJson(Json::Value& target, + bool simplify) const; }; }
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Fri May 29 14:46:55 2015 +0200 @@ -84,6 +84,7 @@ #include "../../Core/OrthancException.h" #include "../ToDcmtkBridge.h" #include "../FromDcmtkBridge.h" +#include "../../Core/DicomFormat/DicomArray.h" #include <dcmtk/dcmdata/dcistrmb.h> #include <dcmtk/dcmdata/dcistrmf.h> @@ -337,6 +338,16 @@ } + namespace + { + struct FindPayload + { + DicomFindAnswers* answers; + std::string level; + }; + } + + static void FindCallback( /* in */ void *callbackData, @@ -346,73 +357,103 @@ DcmDataset *responseIdentifiers /* pending response identifiers */ ) { - DicomFindAnswers& answers = *reinterpret_cast<DicomFindAnswers*>(callbackData); + FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData); if (responseIdentifiers != NULL) { DicomMap m; FromDcmtkBridge::Convert(m, *responseIdentifiers); - answers.Add(m); + + if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) + { + m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level); + } + + payload.answers->Add(m); } } + + static void CheckFindQuery(ResourceType level, + const DicomMap& fields) + { + std::set<DicomTag> allowedTags; + + // WARNING: Do not add "break" or reorder items in this switch-case! + switch (level) + { + case ResourceType_Instance: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance); + + case ResourceType_Series: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Series); + + case ResourceType_Study: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Study); + + case ResourceType_Patient: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (level == ResourceType_Study) + { + allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); + } + + allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET); + + DicomArray query(fields); + for (size_t i = 0; i < query.GetSize(); i++) + { + const DicomTag& tag = query.GetElement(i).GetTag(); + if (allowedTags.find(tag) == allowedTags.end()) + { + LOG(ERROR) << "Tag not allowed for this C-Find level: " << tag; + throw OrthancException(ErrorCode_BadRequest); + } + } + } + + void DicomUserConnection::Find(DicomFindAnswers& result, - FindRootModel model, + ResourceType level, const DicomMap& fields) { + CheckFindQuery(level, fields); + CheckIsOpen(); + FindPayload payload; + payload.answers = &result; + const char* sopClass; std::auto_ptr<DcmDataset> dataset(ToDcmtkBridge::Convert(fields)); - switch (model) + switch (level) { - case FindRootModel_Patient: + case ResourceType_Patient: + payload.level = "PATIENT"; DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT"); sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; - - // Accession number - if (!fields.HasTag(0x0008, 0x0050)) - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0050), ""); - - // Patient ID - if (!fields.HasTag(0x0010, 0x0020)) - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0010, 0x0020), ""); - break; - case FindRootModel_Study: + case ResourceType_Study: + payload.level = "STUDY"; DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY"); sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; - - // Accession number - if (!fields.HasTag(0x0008, 0x0050)) - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0050), ""); - - // Study instance UID - if (!fields.HasTag(0x0020, 0x000d)) - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000d), ""); - break; - case FindRootModel_Series: + case ResourceType_Series: + payload.level = "SERIES"; DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES"); sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; - - // Accession number - if (!fields.HasTag(0x0008, 0x0050)) - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0050), ""); - - // Study instance UID - if (!fields.HasTag(0x0020, 0x000d)) - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000d), ""); - - // Series instance UID - if (!fields.HasTag(0x0020, 0x000e)) - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000e), ""); - break; - case FindRootModel_Instance: + case ResourceType_Instance: + payload.level = "INSTANCE"; if (manufacturer_ == ModalityManufacturer_ClearCanvas || manufacturer_ == ModalityManufacturer_Dcm4Chee) { @@ -427,7 +468,27 @@ } sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // Add the expected tags for this query level. + // WARNING: Do not reorder or add "break" in this switch-case! + switch (level) + { + case ResourceType_Instance: + // SOP Instance UID + if (!fields.HasTag(0x0008, 0x0018)) + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0018), ""); + + case ResourceType_Series: + // Series instance UID + if (!fields.HasTag(0x0020, 0x000e)) + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000e), ""); + + case ResourceType_Study: // Accession number if (!fields.HasTag(0x0008, 0x0050)) DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0050), ""); @@ -436,13 +497,10 @@ if (!fields.HasTag(0x0020, 0x000d)) DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000d), ""); - // Series instance UID - if (!fields.HasTag(0x0020, 0x000e)) - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000e), ""); - - // SOP Instance UID - if (!fields.HasTag(0x0008, 0x0018)) - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0018), ""); + case ResourceType_Patient: + // Patient ID + if (!fields.HasTag(0x0010, 0x0020)) + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0010, 0x0020), ""); break; @@ -467,7 +525,7 @@ T_DIMSE_C_FindRSP response; DcmDataset* statusDetail = NULL; OFCondition cond = DIMSE_findUser(pimpl_->assoc_, presID, &request, dataset.get(), - FindCallback, &result, + FindCallback, &payload, /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, &response, &statusDetail); @@ -481,67 +539,48 @@ } - void DicomUserConnection::FindPatient(DicomFindAnswers& result, - const DicomMap& fields) - { - // Only keep the filters from "fields" that are related to the patient - DicomMap s; - fields.ExtractPatientInformation(s); - Find(result, FindRootModel_Patient, s); - } - - void DicomUserConnection::FindStudy(DicomFindAnswers& result, - const DicomMap& fields) - { - // Only keep the filters from "fields" that are related to the study - DicomMap s; - fields.ExtractStudyInformation(s); - - s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID); - s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER); - s.CopyTagIfExists(fields, DICOM_TAG_MODALITIES_IN_STUDY); - - Find(result, FindRootModel_Study, s); - } - - void DicomUserConnection::FindSeries(DicomFindAnswers& result, - const DicomMap& fields) - { - // Only keep the filters from "fields" that are related to the series - DicomMap s; - fields.ExtractSeriesInformation(s); - - s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID); - s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER); - s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID); - - Find(result, FindRootModel_Series, s); - } - - void DicomUserConnection::FindInstance(DicomFindAnswers& result, + void DicomUserConnection::MoveInternal(const std::string& targetAet, + ResourceType level, const DicomMap& fields) { - // Only keep the filters from "fields" that are related to the instance - DicomMap s; - fields.ExtractInstanceInformation(s); - - s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID); - s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER); - s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID); - s.CopyTagIfExists(fields, DICOM_TAG_SERIES_INSTANCE_UID); - - Find(result, FindRootModel_Instance, s); - } - - - void DicomUserConnection::Move(const std::string& targetAet, - const DicomMap& fields) - { CheckIsOpen(); const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; std::auto_ptr<DcmDataset> dataset(ToDcmtkBridge::Convert(fields)); + switch (level) + { + case ResourceType_Patient: + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT"); + break; + + case ResourceType_Study: + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY"); + break; + + case ResourceType_Series: + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES"); + break; + + case ResourceType_Instance: + if (manufacturer_ == ModalityManufacturer_ClearCanvas || + manufacturer_ == ModalityManufacturer_Dcm4Chee) + { + // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>. + // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J + // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "IMAGE"); + } + else + { + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "INSTANCE"); + } + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + // Figure out which of the accepted presentation contexts should be used int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass); if (presID == 0) @@ -830,33 +869,86 @@ } - void DicomUserConnection::MoveSeries(const std::string& targetAet, - const DicomMap& findResult) + static void TestAndCopyTag(DicomMap& result, + const DicomMap& source, + const DicomTag& tag) + { + if (!source.HasTag(tag)) + { + throw OrthancException(ErrorCode_BadRequest); + } + else + { + result.SetValue(tag, source.GetValue(tag)); + } + } + + + void DicomUserConnection::Move(const std::string& targetAet, + const DicomMap& findResult) { - DicomMap simplified; - simplified.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, findResult.GetValue(DICOM_TAG_STUDY_INSTANCE_UID)); - simplified.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, findResult.GetValue(DICOM_TAG_SERIES_INSTANCE_UID)); - Move(targetAet, simplified); + if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) + { + throw OrthancException(ErrorCode_InternalError); + } + + const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).AsString(); + ResourceType level = StringToResourceType(tmp.c_str()); + + DicomMap move; + switch (level) + { + case ResourceType_Patient: + TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID); + break; + + case ResourceType_Study: + TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + break; + + case ResourceType_Series: + TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); + break; + + case ResourceType_Instance: + TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); + TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + MoveInternal(targetAet, level, move); + } + + + void DicomUserConnection::MovePatient(const std::string& targetAet, + const std::string& patientId) + { + DicomMap query; + query.SetValue(DICOM_TAG_PATIENT_ID, patientId); + MoveInternal(targetAet, ResourceType_Patient, query); + } + + void DicomUserConnection::MoveStudy(const std::string& targetAet, + const std::string& studyUid) + { + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid); + MoveInternal(targetAet, ResourceType_Study, query); } void DicomUserConnection::MoveSeries(const std::string& targetAet, const std::string& studyUid, const std::string& seriesUid) { - DicomMap map; - map.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid); - map.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid); - Move(targetAet, map); - } - - void DicomUserConnection::MoveInstance(const std::string& targetAet, - const DicomMap& findResult) - { - DicomMap simplified; - simplified.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, findResult.GetValue(DICOM_TAG_STUDY_INSTANCE_UID)); - simplified.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, findResult.GetValue(DICOM_TAG_SERIES_INSTANCE_UID)); - simplified.SetValue(DICOM_TAG_SOP_INSTANCE_UID, findResult.GetValue(DICOM_TAG_SOP_INSTANCE_UID)); - Move(targetAet, simplified); + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid); + query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid); + MoveInternal(targetAet, ResourceType_Series, query); } void DicomUserConnection::MoveInstance(const std::string& targetAet, @@ -864,11 +956,11 @@ const std::string& seriesUid, const std::string& instanceUid) { - DicomMap map; - map.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid); - map.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid); - map.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid); - Move(targetAet, map); + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid); + query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid); + query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid); + MoveInternal(targetAet, ResourceType_Instance, query); }
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.h Fri May 29 14:46:55 2015 +0200 @@ -46,14 +46,6 @@ class DicomUserConnection : public boost::noncopyable { private: - enum FindRootModel - { - FindRootModel_Patient, - FindRootModel_Study, - FindRootModel_Series, - FindRootModel_Instance - }; - struct PImpl; boost::shared_ptr<PImpl> pimpl_; @@ -72,12 +64,9 @@ void SetupPresentationContexts(const std::string& preferredTransferSyntax); - void Find(DicomFindAnswers& result, - FindRootModel model, - const DicomMap& fields); - - void Move(const std::string& targetAet, - const DicomMap& fields); + void MoveInternal(const std::string& targetAet, + ResourceType level, + const DicomMap& fields); void ResetStorageSOPClasses(); @@ -150,29 +139,24 @@ void StoreFile(const std::string& path); - void FindPatient(DicomFindAnswers& result, - const DicomMap& fields); - - void FindStudy(DicomFindAnswers& result, - const DicomMap& fields); + void Find(DicomFindAnswers& result, + ResourceType level, + const DicomMap& fields); - void FindSeries(DicomFindAnswers& result, - const DicomMap& fields); + void Move(const std::string& targetAet, + const DicomMap& findResult); - void FindInstance(DicomFindAnswers& result, - const DicomMap& fields); + void MovePatient(const std::string& targetAet, + const std::string& patientId); - void MoveSeries(const std::string& targetAet, - const DicomMap& findResult); + void MoveStudy(const std::string& targetAet, + const std::string& studyUid); void MoveSeries(const std::string& targetAet, const std::string& studyUid, const std::string& seriesUid); void MoveInstance(const std::string& targetAet, - const DicomMap& findResult); - - void MoveInstance(const std::string& targetAet, const std::string& studyUid, const std::string& seriesUid, const std::string& instanceUid);
--- a/OrthancServer/FromDcmtkBridge.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/FromDcmtkBridge.cpp Fri May 29 14:46:55 2015 +0200 @@ -624,7 +624,8 @@ void FromDcmtkBridge::ToJson(Json::Value& result, - const DicomMap& values) + const DicomMap& values, + bool simplify) { if (result.type() != Json::objectValue) { @@ -636,7 +637,29 @@ for (DicomMap::Map::const_iterator it = values.map_.begin(); it != values.map_.end(); ++it) { - result[GetName(it->first)] = it->second->AsString(); + if (simplify) + { + result[GetName(it->first)] = it->second->AsString(); + } + else + { + Json::Value value = Json::objectValue; + + value["Name"] = GetName(it->first); + + if (it->second->IsNull()) + { + value["Type"] = "Null"; + value["Value"] = Json::nullValue; + } + else + { + value["Type"] = "String"; + value["Value"] = it->second->AsString(); + } + + result[it->first.Format()] = value; + } } }
--- a/OrthancServer/FromDcmtkBridge.h Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/FromDcmtkBridge.h Fri May 29 14:46:55 2015 +0200 @@ -99,7 +99,8 @@ const DicomMap& m); static void ToJson(Json::Value& result, - const DicomMap& values); + const DicomMap& values, + bool simplify); static std::string GenerateUniqueIdentifier(ResourceType level);
--- a/OrthancServer/OrthancInitialization.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Fri May 29 14:46:55 2015 +0200 @@ -238,7 +238,8 @@ { boost::mutex::scoped_lock lock(globalMutex_); - if (configuration_->isMember(parameter)) + if (configuration_.get() != NULL && + configuration_->isMember(parameter)) { return (*configuration_) [parameter].asString(); } @@ -254,7 +255,8 @@ { boost::mutex::scoped_lock lock(globalMutex_); - if (configuration_->isMember(parameter)) + if (configuration_.get() != NULL && + configuration_->isMember(parameter)) { return (*configuration_) [parameter].asInt(); } @@ -270,7 +272,8 @@ { boost::mutex::scoped_lock lock(globalMutex_); - if (configuration_->isMember(parameter)) + if (configuration_.get() != NULL && + configuration_->isMember(parameter)) { return (*configuration_) [parameter].asBool(); } @@ -286,6 +289,11 @@ { boost::mutex::scoped_lock lock(globalMutex_); + if (configuration_.get() == NULL) + { + throw OrthancException(ErrorCode_InexistentItem); + } + if (!configuration_->isMember("DicomModalities")) { throw OrthancException(ErrorCode_BadFileFormat); @@ -318,6 +326,11 @@ { boost::mutex::scoped_lock lock(globalMutex_); + if (configuration_.get() == NULL) + { + throw OrthancException(ErrorCode_InexistentItem); + } + if (!configuration_->isMember("OrthancPeers")) { throw OrthancException(ErrorCode_BadFileFormat); @@ -352,7 +365,8 @@ target.clear(); - if (!configuration_->isMember(parameter)) + if (configuration_.get() == NULL || + !configuration_->isMember(parameter)) { return true; } @@ -409,7 +423,8 @@ httpServer.ClearUsers(); - if (!configuration_->isMember("RegisteredUsers")) + if (configuration_.get() == NULL || + !configuration_->isMember("RegisteredUsers")) { return; } @@ -470,7 +485,8 @@ target.clear(); - if (!configuration_->isMember(key)) + if (configuration_.get() == NULL || + !configuration_->isMember(key)) { return; } @@ -571,6 +587,11 @@ { boost::mutex::scoped_lock lock(globalMutex_); + if (configuration_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + if (!configuration_->isMember("DicomModalities")) { (*configuration_) ["DicomModalities"] = Json::objectValue; @@ -594,6 +615,11 @@ { boost::mutex::scoped_lock lock(globalMutex_); + if (configuration_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + if (!configuration_->isMember("DicomModalities")) { throw OrthancException(ErrorCode_BadFileFormat); @@ -614,6 +640,11 @@ { boost::mutex::scoped_lock lock(globalMutex_); + if (configuration_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + if (!configuration_->isMember("OrthancPeers")) { (*configuration_) ["OrthancPeers"] = Json::objectValue; @@ -637,6 +668,11 @@ { boost::mutex::scoped_lock lock(globalMutex_); + if (configuration_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + if (!configuration_->isMember("OrthancPeers")) { throw OrthancException(ErrorCode_BadFileFormat);
--- a/OrthancServer/OrthancMoveRequestHandler.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Fri May 29 14:46:55 2015 +0200 @@ -36,6 +36,8 @@ #include <glog/logging.h> #include "OrthancInitialization.h" +#include "FromDcmtkBridge.h" +#include "../Core/DicomFormat/DicomArray.h" namespace Orthanc { @@ -132,6 +134,21 @@ { LOG(WARNING) << "Move-SCU request received for AET \"" << aet << "\""; + { + DicomArray query(input); + for (size_t i = 0; i < query.GetSize(); i++) + { + if (!query.GetElement(i).GetValue().IsNull()) + { + LOG(INFO) << " " << query.GetElement(i).GetTag() + << " " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag()) + << " = " << query.GetElement(i).GetValue().AsString(); + } + } + } + + + /** * Retrieve the query level. **/
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Fri May 29 14:46:55 2015 +0200 @@ -39,35 +39,16 @@ #include "../Scheduler/ServerJob.h" #include "../Scheduler/StoreScuCommand.h" #include "../Scheduler/StorePeerCommand.h" +#include "../QueryRetrieveHandler.h" +#include "../ServerToolbox.h" #include <glog/logging.h> namespace Orthanc { - // DICOM SCU ---------------------------------------------------------------- - - static bool MergeQueryAndTemplate(DicomMap& result, - const std::string& postData) - { - Json::Value query; - Json::Reader reader; - - if (!reader.parse(postData, query) || - query.type() != Json::objectValue) - { - return false; - } - - Json::Value::Members members = query.getMemberNames(); - for (size_t i = 0; i < members.size(); i++) - { - DicomTag t = FromDcmtkBridge::ParseTag(members[i]); - result.SetValue(t, query[members[i]].asString()); - } - - return true; - } - + /*************************************************************************** + * DICOM C-Echo SCU + ***************************************************************************/ static void DicomEcho(RestApiPostCall& call) { @@ -94,13 +75,100 @@ } + + /*************************************************************************** + * DICOM C-Find SCU => DEPRECATED! + ***************************************************************************/ + + static bool MergeQueryAndTemplate(DicomMap& result, + const std::string& postData) + { + Json::Value query; + Json::Reader reader; + + if (!reader.parse(postData, query) || + query.type() != Json::objectValue) + { + return false; + } + + Json::Value::Members members = query.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + DicomTag t = FromDcmtkBridge::ParseTag(members[i]); + result.SetValue(t, query[members[i]].asString()); + } + + return true; + } + + + static void FindPatient(DicomFindAnswers& result, + DicomUserConnection& connection, + const DicomMap& fields) + { + // Only keep the filters from "fields" that are related to the patient + DicomMap s; + fields.ExtractPatientInformation(s); + connection.Find(result, ResourceType_Patient, s); + } + + + static void FindStudy(DicomFindAnswers& result, + DicomUserConnection& connection, + const DicomMap& fields) + { + // Only keep the filters from "fields" that are related to the study + DicomMap s; + fields.ExtractStudyInformation(s); + + s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID); + s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER); + s.CopyTagIfExists(fields, DICOM_TAG_MODALITIES_IN_STUDY); + + connection.Find(result, ResourceType_Study, s); + } + + static void FindSeries(DicomFindAnswers& result, + DicomUserConnection& connection, + const DicomMap& fields) + { + // Only keep the filters from "fields" that are related to the series + DicomMap s; + fields.ExtractSeriesInformation(s); + + s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID); + s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER); + s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID); + + connection.Find(result, ResourceType_Series, s); + } + + static void FindInstance(DicomFindAnswers& result, + DicomUserConnection& connection, + const DicomMap& fields) + { + // Only keep the filters from "fields" that are related to the instance + DicomMap s; + fields.ExtractInstanceInformation(s); + + s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID); + s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER); + s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID); + s.CopyTagIfExists(fields, DICOM_TAG_SERIES_INSTANCE_UID); + + connection.Find(result, ResourceType_Instance, s); + } + + static void DicomFindPatient(RestApiPostCall& call) { + LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); ServerContext& context = OrthancRestApi::GetContext(call); - DicomMap m; - DicomMap::SetupFindPatientTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) + DicomMap fields; + DicomMap::SetupFindPatientTemplate(fields); + if (!MergeQueryAndTemplate(fields, call.GetPostBody())) { return; } @@ -109,26 +177,27 @@ ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote); DicomFindAnswers answers; - locker.GetConnection().FindPatient(answers, m); + FindPatient(answers, locker.GetConnection(), fields); Json::Value result; - answers.ToJson(result); + answers.ToJson(result, true); call.GetOutput().AnswerJson(result); } static void DicomFindStudy(RestApiPostCall& call) { + LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); ServerContext& context = OrthancRestApi::GetContext(call); - DicomMap m; - DicomMap::SetupFindStudyTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) + DicomMap fields; + DicomMap::SetupFindStudyTemplate(fields); + if (!MergeQueryAndTemplate(fields, call.GetPostBody())) { return; } - if (m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && - m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) + if (fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && + fields.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) { return; } @@ -137,27 +206,28 @@ ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote); DicomFindAnswers answers; - locker.GetConnection().FindStudy(answers, m); + FindStudy(answers, locker.GetConnection(), fields); Json::Value result; - answers.ToJson(result); + answers.ToJson(result, true); call.GetOutput().AnswerJson(result); } static void DicomFindSeries(RestApiPostCall& call) { + LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); ServerContext& context = OrthancRestApi::GetContext(call); - DicomMap m; - DicomMap::SetupFindSeriesTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) + DicomMap fields; + DicomMap::SetupFindSeriesTemplate(fields); + if (!MergeQueryAndTemplate(fields, call.GetPostBody())) { return; } - if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && - m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || - m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2) + if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && + fields.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || + fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2) { return; } @@ -166,28 +236,29 @@ ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote); DicomFindAnswers answers; - locker.GetConnection().FindSeries(answers, m); + FindSeries(answers, locker.GetConnection(), fields); Json::Value result; - answers.ToJson(result); + answers.ToJson(result, true); call.GetOutput().AnswerJson(result); } static void DicomFindInstance(RestApiPostCall& call) { + LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); ServerContext& context = OrthancRestApi::GetContext(call); - DicomMap m; - DicomMap::SetupFindInstanceTemplate(m); - if (!MergeQueryAndTemplate(m, call.GetPostBody())) + DicomMap fields; + DicomMap::SetupFindInstanceTemplate(fields); + if (!MergeQueryAndTemplate(fields, call.GetPostBody())) { return; } - if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && - m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || - m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2 || - m.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString().size() <= 2) + if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && + fields.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || + fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2 || + fields.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString().size() <= 2) { return; } @@ -196,15 +267,17 @@ ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote); DicomFindAnswers answers; - locker.GetConnection().FindInstance(answers, m); + FindInstance(answers, locker.GetConnection(), fields); Json::Value result; - answers.ToJson(result); + answers.ToJson(result, true); call.GetOutput().AnswerJson(result); } + static void DicomFind(RestApiPostCall& call) { + LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); ServerContext& context = OrthancRestApi::GetContext(call); DicomMap m; @@ -218,14 +291,14 @@ ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote); DicomFindAnswers patients; - locker.GetConnection().FindPatient(patients, m); + FindPatient(patients, locker.GetConnection(), m); // Loop over the found patients Json::Value result = Json::arrayValue; for (size_t i = 0; i < patients.GetSize(); i++) { Json::Value patient(Json::objectValue); - FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i)); + FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i), true); DicomMap::SetupFindStudyTemplate(m); if (!MergeQueryAndTemplate(m, call.GetPostBody())) @@ -235,7 +308,7 @@ m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); DicomFindAnswers studies; - locker.GetConnection().FindStudy(studies, m); + FindStudy(studies, locker.GetConnection(), m); patient["Studies"] = Json::arrayValue; @@ -243,7 +316,7 @@ for (size_t j = 0; j < studies.GetSize(); j++) { Json::Value study(Json::objectValue); - FromDcmtkBridge::ToJson(study, studies.GetAnswer(j)); + FromDcmtkBridge::ToJson(study, studies.GetAnswer(j), true); DicomMap::SetupFindSeriesTemplate(m); if (!MergeQueryAndTemplate(m, call.GetPostBody())) @@ -254,14 +327,14 @@ m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); DicomFindAnswers series; - locker.GetConnection().FindSeries(series, m); + FindSeries(series, locker.GetConnection(), m); // Loop over the found series study["Series"] = Json::arrayValue; for (size_t k = 0; k < series.GetSize(); k++) { Json::Value series2(Json::objectValue); - FromDcmtkBridge::ToJson(series2, series.GetAnswer(k)); + FromDcmtkBridge::ToJson(series2, series.GetAnswer(k), true); study["Series"].append(series2); } @@ -275,6 +348,205 @@ } + + /*************************************************************************** + * DICOM C-Find and C-Move SCU => Recommended since Orthanc 0.9.0 + ***************************************************************************/ + + static void DicomQuery(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + Json::Value request; + + if (call.ParseJsonRequest(request) && + request.type() == Json::objectValue && + request.isMember("Level") && request["Level"].type() == Json::stringValue && + (!request.isMember("Query") || request["Query"].type() == Json::objectValue)) + { + std::auto_ptr<QueryRetrieveHandler> handler(new QueryRetrieveHandler(context)); + + handler->SetModality(call.GetUriComponent("id", "")); + handler->SetLevel(StringToResourceType(request["Level"].asString().c_str())); + + if (request.isMember("Query")) + { + Json::Value::Members tags = request["Query"].getMemberNames(); + for (size_t i = 0; i < tags.size(); i++) + { + handler->SetQuery(FromDcmtkBridge::ParseTag(tags[i].c_str()), + request["Query"][tags[i]].asString()); + } + } + + handler->Run(); + + std::string s = context.GetQueryRetrieveArchive().Add(handler.release()); + Json::Value result = Json::objectValue; + result["ID"] = s; + result["Path"] = "/queries/" + s; + call.GetOutput().AnswerJson(result); + } + } + + + static void ListQueries(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::list<std::string> queries; + context.GetQueryRetrieveArchive().List(queries); + + Json::Value result = Json::arrayValue; + for (std::list<std::string>::const_iterator + it = queries.begin(); it != queries.end(); ++it) + { + result.append(*it); + } + + call.GetOutput().AnswerJson(result); + } + + + namespace + { + class QueryAccessor + { + private: + ServerContext& context_; + SharedArchive::Accessor accessor_; + QueryRetrieveHandler& handler_; + + public: + QueryAccessor(RestApiCall& call) : + context_(OrthancRestApi::GetContext(call)), + accessor_(context_.GetQueryRetrieveArchive(), call.GetUriComponent("id", "")), + handler_(dynamic_cast<QueryRetrieveHandler&>(accessor_.GetItem())) + { + } + + QueryRetrieveHandler* operator->() + { + return &handler_; + } + }; + + static void AnswerDicomMap(RestApiCall& call, + const DicomMap& value, + bool simplify) + { + Json::Value full = Json::objectValue; + FromDcmtkBridge::ToJson(full, value, simplify); + call.GetOutput().AnswerJson(full); + } + } + + + static void ListQueryAnswers(RestApiGetCall& call) + { + QueryAccessor query(call); + size_t count = query->GetAnswerCount(); + + Json::Value result = Json::arrayValue; + for (size_t i = 0; i < count; i++) + { + result.append(boost::lexical_cast<std::string>(i)); + } + + call.GetOutput().AnswerJson(result); + } + + + static void GetQueryOneAnswer(RestApiGetCall& call) + { + size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", "")); + QueryAccessor query(call); + AnswerDicomMap(call, query->GetAnswer(index), call.HasArgument("simplify")); + } + + + static void RetrieveOneAnswer(RestApiPostCall& call) + { + size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", "")); + + LOG(WARNING) << "Driving C-Move SCU on modality: " << call.GetPostBody(); + + QueryAccessor query(call); + query->Retrieve(call.GetPostBody(), index); + + // Retrieve has succeeded + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + + + static void RetrieveAllAnswers(RestApiPostCall& call) + { + LOG(WARNING) << "Driving C-Move SCU on modality: " << call.GetPostBody(); + + QueryAccessor query(call); + query->Retrieve(call.GetPostBody()); + + // Retrieve has succeeded + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + + + static void GetQueryArguments(RestApiGetCall& call) + { + QueryAccessor query(call); + AnswerDicomMap(call, query->GetQuery(), call.HasArgument("simplify")); + } + + + static void GetQueryLevel(RestApiGetCall& call) + { + QueryAccessor query(call); + call.GetOutput().AnswerBuffer(EnumerationToString(query->GetLevel()), "text/plain"); + } + + + static void GetQueryModality(RestApiGetCall& call) + { + QueryAccessor query(call); + call.GetOutput().AnswerBuffer(query->GetModalitySymbolicName(), "text/plain"); + } + + + static void DeleteQuery(RestApiDeleteCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + context.GetQueryRetrieveArchive().Remove(call.GetUriComponent("id", "")); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + + + static void ListQueryOperations(RestApiGetCall& call) + { + // Ensure that the query of interest does exist + QueryAccessor query(call); + + RestApi::AutoListChildren(call); + } + + + static void ListQueryAnswerOperations(RestApiGetCall& call) + { + // Ensure that the query of interest does exist + QueryAccessor query(call); + + // Ensure that the answer of interest does exist + size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", "")); + query->GetAnswer(index); + + RestApi::AutoListChildren(call); + } + + + + + /*************************************************************************** + * DICOM C-Store SCU + ***************************************************************************/ + static bool GetInstancesToExport(std::list<std::string>& instances, const std::string& remote, RestApiPostCall& call) @@ -379,7 +651,9 @@ } - // Orthanc Peers ------------------------------------------------------------ + /*************************************************************************** + * Orthanc Peers => Store client + ***************************************************************************/ static bool IsExistingPeer(const OrthancRestApi::SetOfStrings& peers, const std::string& id) @@ -543,6 +817,20 @@ Register("/modalities/{id}/find", DicomFind); Register("/modalities/{id}/store", DicomStore); + // For Query/Retrieve + Register("/modalities/{id}/query", DicomQuery); + Register("/queries", ListQueries); + Register("/queries/{id}", DeleteQuery); + Register("/queries/{id}", ListQueryOperations); + Register("/queries/{id}/answers", ListQueryAnswers); + Register("/queries/{id}/answers/{index}", ListQueryAnswerOperations); + Register("/queries/{id}/answers/{index}/content", GetQueryOneAnswer); + Register("/queries/{id}/answers/{index}/retrieve", RetrieveOneAnswer); + Register("/queries/{id}/level", GetQueryLevel); + Register("/queries/{id}/modality", GetQueryModality); + Register("/queries/{id}/query", GetQueryArguments); + Register("/queries/{id}/retrieve", RetrieveAllAnswers); + Register("/peers", ListPeers); Register("/peers/{id}", ListPeerOperations); Register("/peers/{id}", UpdatePeer);
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Fri May 29 14:46:55 2015 +0200 @@ -765,7 +765,7 @@ typedef std::set<DicomTag> ModuleTags; ModuleTags moduleTags; - DicomTag::GetTagsForModule(moduleTags, module); + DicomTag::AddTagsForModule(moduleTags, module); Json::Value tags;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/QueryRetrieveHandler.cpp Fri May 29 14:46:55 2015 +0200 @@ -0,0 +1,132 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "PrecompiledHeadersServer.h" +#include "QueryRetrieveHandler.h" + +#include "OrthancInitialization.h" + + +namespace Orthanc +{ + void QueryRetrieveHandler::Invalidate() + { + done_ = false; + answers_.Clear(); + } + + + void QueryRetrieveHandler::Run() + { + if (!done_) + { + ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), modality_); + locker.GetConnection().Find(answers_, level_, query_); + done_ = true; + } + } + + + QueryRetrieveHandler::QueryRetrieveHandler(ServerContext& context) : + context_(context), + done_(false), + level_(ResourceType_Study) + { + } + + + void QueryRetrieveHandler::SetModality(const std::string& symbolicName) + { + Invalidate(); + modalityName_ = symbolicName; + Configuration::GetDicomModalityUsingSymbolicName(modality_, symbolicName); + } + + + void QueryRetrieveHandler::SetLevel(ResourceType level) + { + Invalidate(); + level_ = level; + } + + + void QueryRetrieveHandler::SetQuery(const DicomTag& tag, + const std::string& value) + { + Invalidate(); + query_.SetValue(tag, value); + } + + + size_t QueryRetrieveHandler::GetAnswerCount() + { + Run(); + return answers_.GetSize(); + } + + + const DicomMap& QueryRetrieveHandler::GetAnswer(size_t i) + { + Run(); + + if (i >= answers_.GetSize()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + return answers_.GetAnswer(i); + } + + + void QueryRetrieveHandler::Retrieve(const std::string& target, + size_t i) + { + Run(); + + if (i >= answers_.GetSize()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), modality_); + locker.GetConnection().Move(target, answers_.GetAnswer(i)); + } + + + void QueryRetrieveHandler::Retrieve(const std::string& target) + { + for (size_t i = 0; i < GetAnswerCount(); i++) + { + Retrieve(target, i); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/QueryRetrieveHandler.h Fri May 29 14:46:55 2015 +0200 @@ -0,0 +1,94 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "ServerContext.h" + +namespace Orthanc +{ + class QueryRetrieveHandler : public IDynamicObject + { + private: + ServerContext& context_; + bool done_; + RemoteModalityParameters modality_; + ResourceType level_; + DicomMap query_; + DicomFindAnswers answers_; + std::string modalityName_; + + void Invalidate(); + + + public: + QueryRetrieveHandler(ServerContext& context); + + void SetModality(const std::string& symbolicName); + + const RemoteModalityParameters& GetModality() const + { + return modality_; + } + + const std::string& GetModalitySymbolicName() const + { + return modalityName_; + } + + void SetLevel(ResourceType level); + + ResourceType GetLevel() const + { + return level_; + } + + void SetQuery(const DicomTag& tag, + const std::string& value); + + const DicomMap& GetQuery() const + { + return query_; + } + + void Run(); + + size_t GetAnswerCount(); + + const DicomMap& GetAnswer(size_t i); + + void Retrieve(const std::string& target, + size_t i); + + void Retrieve(const std::string& target); + }; +}
--- a/OrthancServer/ServerContext.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/ServerContext.cpp Fri May 29 14:46:55 2015 +0200 @@ -78,7 +78,8 @@ dicomCache_(provider_, DICOM_CACHE_SIZE), scheduler_(Configuration::GetGlobalIntegerParameter("LimitJobs", 10)), plugins_(NULL), - pluginsManager_(NULL) + pluginsManager_(NULL), + queryRetrieveArchive_(Configuration::GetGlobalIntegerParameter("QueryRetrieveSize", 10)) { scu_.SetLocalApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"));
--- a/OrthancServer/ServerContext.h Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/ServerContext.h Fri May 29 14:46:55 2015 +0200 @@ -43,6 +43,7 @@ #include "Scheduler/ServerScheduler.h" #include "DicomInstanceToStore.h" #include "ServerIndexChange.h" +#include "../Core/Cache/SharedArchive.h" #include <boost/filesystem.hpp> @@ -96,6 +97,8 @@ OrthancPlugins* plugins_; // TODO Turn it into a listener pattern (idem for Lua callbacks) const PluginsManager* pluginsManager_; + SharedArchive queryRetrieveArchive_; + public: class DicomCacheLocker : public boost::noncopyable { @@ -223,5 +226,10 @@ const PluginsManager& GetPluginsManager() const; const OrthancPlugins& GetOrthancPlugins() const; + + SharedArchive& GetQueryRetrieveArchive() + { + return queryRetrieveArchive_; + } }; }
--- a/OrthancServer/ServerIndex.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/ServerIndex.cpp Fri May 29 14:46:55 2015 +0200 @@ -883,7 +883,7 @@ DicomMap tags; db_.GetMainDicomTags(tags, resourceId); target["MainDicomTags"] = Json::objectValue; - FromDcmtkBridge::ToJson(target["MainDicomTags"], tags); + FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true); } bool ServerIndex::LookupResource(Json::Value& result,
--- a/OrthancServer/main.cpp Fri May 29 14:46:06 2015 +0200 +++ b/OrthancServer/main.cpp Fri May 29 14:46:55 2015 +0200 @@ -51,6 +51,7 @@ #include "ServerToolbox.h" #include "../Plugins/Engine/PluginsManager.h" #include "../Plugins/Engine/OrthancPlugins.h" +#include "FromDcmtkBridge.h" using namespace Orthanc; @@ -383,6 +384,8 @@ + + static bool StartOrthanc(int argc, char *argv[]) { #if ENABLE_PLUGINS == 1 @@ -531,6 +534,7 @@ } LOG(WARNING) << "Orthanc has started"; + Toolbox::ServerBarrier(restApi.ResetRequestReceivedFlag()); isReset = restApi.ResetRequestReceivedFlag();
--- a/Resources/Configuration.json Fri May 29 14:46:06 2015 +0200 +++ b/Resources/Configuration.json Fri May 29 14:46:55 2015 +0200 @@ -224,6 +224,11 @@ // to 0, the connection is closed immediately. "DicomAssociationCloseDelay" : 5, + // Maximum number of query/retrieve DICOM requests that are + // maintained by Orthanc. The least recently used requests get + // deleted as new requests are issued. + "QueryRetrieveSize" : 10, + // When handling a C-Find SCP request, setting this flag to "false" // will enable case-insensitive match for PN value representation // (such as PatientName). By default, the search is case-insensitive.
--- a/Resources/DicomConformanceStatement.txt Fri May 29 14:46:06 2015 +0200 +++ b/Resources/DicomConformanceStatement.txt Fri May 29 14:46:55 2015 +0200 @@ -186,7 +186,16 @@ FINDPatientRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.1.1 FINDStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.1 - FINDStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.1 + + +-------------------- +Move SCU Conformance +-------------------- + +Orthanc supports the following SOP Classes as an SCU for C-Move: + + MOVEPatientRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.1.2 + MOVEStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.2 -----------------
--- a/UnitTestsSources/DicomMapTests.cpp Fri May 29 14:46:06 2015 +0200 +++ b/UnitTestsSources/DicomMapTests.cpp Fri May 29 14:46:55 2015 +0200 @@ -153,7 +153,7 @@ DicomModule module) { std::set<DicomTag> moduleTags, main; - DicomTag::GetTagsForModule(moduleTags, module); + DicomTag::AddTagsForModule(moduleTags, module); DicomMap::GetMainDicomTags(main, level); // The main dicom tags are a subset of the module
--- a/UnitTestsSources/MemoryCacheTests.cpp Fri May 29 14:46:06 2015 +0200 +++ b/UnitTestsSources/MemoryCacheTests.cpp Fri May 29 14:46:55 2015 +0200 @@ -39,6 +39,7 @@ #include <boost/lexical_cast.hpp> #include "../Core/IDynamicObject.h" #include "../Core/Cache/MemoryCache.h" +#include "../Core/Cache/SharedArchive.h" TEST(LRU, Basic) @@ -228,3 +229,66 @@ ASSERT_EQ("45 42 43 47 44 42 ", provider.log_); } + + + + + + + +namespace +{ + class S : public Orthanc::IDynamicObject + { + private: + std::string value_; + + public: + S(const std::string& value) : value_(value) + { + } + + const std::string& GetValue() const + { + return value_; + } + + static const std::string& Access(const Orthanc::IDynamicObject& obj) + { + return dynamic_cast<const S&>(obj).GetValue(); + } + }; +} + + +TEST(LRU, SharedArchive) +{ + std::string first, second; + Orthanc::SharedArchive a(3); + first = a.Add(new S("First item")); + second = a.Add(new S("Second item")); + + for (int i = 1; i < 100; i++) + { + a.Add(new S("Item " + boost::lexical_cast<std::string>(i))); + // Continuously protect the two first items + try { Orthanc::SharedArchive::Accessor(a, first); } catch (Orthanc::OrthancException&) {} + try { Orthanc::SharedArchive::Accessor(a, second); } catch (Orthanc::OrthancException&) {} + } + + std::list<std::string> i; + a.List(i); + + size_t count = 0; + for (std::list<std::string>::const_iterator + it = i.begin(); it != i.end(); it++) + { + if (*it == first || + *it == second) + { + count++; + } + } + + ASSERT_EQ(2, count); +}