# HG changeset patch # User Sebastien Jodogne # Date 1432722763 -7200 # Node ID 7b6f5115607f82f039b1370f83e69305962e65c7 # Parent b22ba8c5edbefe2b12e6d06926c56ccfe33ed3e2# Parent 4460e2622016b97b720600adb508452eba034444 integration mainline->query-retrieve diff -r 4460e2622016 -r 7b6f5115607f CMakeLists.txt --- a/CMakeLists.txt Wed May 27 10:50:59 2015 +0200 +++ b/CMakeLists.txt Wed May 27 12:32:43 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 diff -r 4460e2622016 -r 7b6f5115607f Core/Cache/SharedArchive.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/SharedArchive.cpp Wed May 27 12:32:43 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 . + **/ + + +#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& 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); + } + } +} + + diff -r 4460e2622016 -r 7b6f5115607f Core/Cache/SharedArchive.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/SharedArchive.h Wed May 27 12:32:43 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 . + **/ + + +#pragma once + +#include "LeastRecentlyUsedIndex.h" +#include "../IDynamicObject.h" + +#include +#include + +namespace Orthanc +{ + class SharedArchive : public boost::noncopyable + { + private: + typedef std::map Archive; + + size_t maxSize_; + boost::mutex mutex_; + Archive archive_; + Orthanc::LeastRecentlyUsedIndex 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& items); + }; +} + + diff -r 4460e2622016 -r 7b6f5115607f Core/DicomFormat/DicomMap.cpp diff -r 4460e2622016 -r 7b6f5115607f Core/DicomFormat/DicomMap.h diff -r 4460e2622016 -r 7b6f5115607f Core/DicomFormat/DicomTag.cpp --- a/Core/DicomFormat/DicomTag.cpp Wed May 27 10:50:59 2015 +0200 +++ b/Core/DicomFormat/DicomTag.cpp Wed May 27 12:32:43 2015 +0200 @@ -118,11 +118,10 @@ } - void DicomTag::GetTagsForModule(std::set& target, + void DicomTag::AddTagsForModule(std::set& target, DicomModule module) { // REFERENCE: 11_03pu.pdf, DICOM PS 3.3 2011 - Information Object Definitions - target.clear(); switch (module) { diff -r 4460e2622016 -r 7b6f5115607f Core/DicomFormat/DicomTag.h --- a/Core/DicomFormat/DicomTag.h Wed May 27 10:50:59 2015 +0200 +++ b/Core/DicomFormat/DicomTag.h Wed May 27 12:32:43 2015 +0200 @@ -84,7 +84,7 @@ friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag); - static void GetTagsForModule(std::set& target, + static void AddTagsForModule(std::set& target, DicomModule module); bool IsIdentifier() const; diff -r 4460e2622016 -r 7b6f5115607f Core/RestApi/RestApi.cpp --- a/Core/RestApi/RestApi.cpp Wed May 27 10:50:59 2015 +0200 +++ b/Core/RestApi/RestApi.cpp Wed May 27 12:32:43 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 diff -r 4460e2622016 -r 7b6f5115607f Core/RestApi/RestApiCall.cpp --- a/Core/RestApi/RestApiCall.cpp Wed May 27 10:50:59 2015 +0200 +++ b/Core/RestApi/RestApiCall.cpp Wed May 27 12:32:43 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; + } } diff -r 4460e2622016 -r 7b6f5115607f Core/RestApi/RestApiCall.h --- a/Core/RestApi/RestApiCall.h Wed May 27 10:50:59 2015 +0200 +++ b/Core/RestApi/RestApiCall.h Wed May 27 12:32:43 2015 +0200 @@ -114,6 +114,8 @@ HttpHandler::ParseCookies(result, httpHeaders_); } + std::string FlattenUri() const; + virtual bool ParseJsonRequest(Json::Value& result) const = 0; }; } diff -r 4460e2622016 -r 7b6f5115607f Core/RestApi/RestApiOutput.cpp --- a/Core/RestApi/RestApiOutput.cpp Wed May 27 10:50:59 2015 +0200 +++ b/Core/RestApi/RestApiOutput.cpp Wed May 27 12:32:43 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); + } } } diff -r 4460e2622016 -r 7b6f5115607f Core/RestApi/RestApiOutput.h --- a/Core/RestApi/RestApiOutput.h Wed May 27 10:50:59 2015 +0200 +++ b/Core/RestApi/RestApiOutput.h Wed May 27 12:32:43 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(); diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/DicomProtocol/DicomFindAnswers.cpp --- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Wed May 27 12:32:43 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); } } diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/DicomProtocol/DicomFindAnswers.h --- a/OrthancServer/DicomProtocol/DicomFindAnswers.h Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h Wed May 27 12:32:43 2015 +0200 @@ -69,6 +69,7 @@ return *items_.at(index); } - void ToJson(Json::Value& target) const; + void ToJson(Json::Value& target, + bool simplify) const; }; } diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/DicomProtocol/DicomUserConnection.cpp --- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Wed May 27 12:32:43 2015 +0200 @@ -84,6 +84,7 @@ #include "../../Core/OrthancException.h" #include "../ToDcmtkBridge.h" #include "../FromDcmtkBridge.h" +#include "../../Core/DicomFormat/DicomArray.h" #include #include @@ -337,6 +338,16 @@ } + namespace + { + struct FindPayload + { + DicomFindAnswers* answers; + std::string level; + }; + } + + static void FindCallback( /* in */ void *callbackData, @@ -346,73 +357,96 @@ DcmDataset *responseIdentifiers /* pending response identifiers */ ) { - DicomFindAnswers& answers = *reinterpret_cast(callbackData); + FindPayload& payload = *reinterpret_cast(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 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); + } + + 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 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 +461,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 +490,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 +518,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,62 +532,9 @@ } - 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, 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; @@ -830,33 +828,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, move); + } + + + void DicomUserConnection::MovePatient(const std::string& targetAet, + const std::string& patientId) + { + DicomMap query; + query.SetValue(DICOM_TAG_PATIENT_ID, patientId); + MoveInternal(targetAet, query); + } + + void DicomUserConnection::MoveStudy(const std::string& targetAet, + const std::string& studyUid) + { + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid); + MoveInternal(targetAet, 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, query); } void DicomUserConnection::MoveInstance(const std::string& targetAet, @@ -864,11 +915,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, query); } diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/DicomProtocol/DicomUserConnection.h --- a/OrthancServer/DicomProtocol/DicomUserConnection.h Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.h Wed May 27 12:32:43 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_; @@ -72,12 +64,8 @@ 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, + const DicomMap& fields); void ResetStorageSOPClasses(); @@ -150,29 +138,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); diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/FromDcmtkBridge.cpp --- a/OrthancServer/FromDcmtkBridge.cpp Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/FromDcmtkBridge.cpp Wed May 27 12:32:43 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; + } } } diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/FromDcmtkBridge.h --- a/OrthancServer/FromDcmtkBridge.h Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/FromDcmtkBridge.h Wed May 27 12:32:43 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); diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/OrthancFindRequestHandler.cpp diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Wed May 27 12:32:43 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); diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed May 27 12:32:43 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 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 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 queries; + context.GetQueryRetrieveArchive().List(queries); + + Json::Value result = Json::arrayValue; + for (std::list::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(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(i)); + } + + call.GetOutput().AnswerJson(result); + } + + + static void GetQueryOneAnswer(RestApiGetCall& call) + { + size_t index = boost::lexical_cast(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(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(call.GetUriComponent("index", "")); + query->GetAnswer(index); + + RestApi::AutoListChildren(call); + } + + + + + /*************************************************************************** + * DICOM C-Store SCU + ***************************************************************************/ + static bool GetInstancesToExport(std::list& 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); diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed May 27 12:32:43 2015 +0200 @@ -765,7 +765,7 @@ typedef std::set ModuleTags; ModuleTags moduleTags; - DicomTag::GetTagsForModule(moduleTags, module); + DicomTag::AddTagsForModule(moduleTags, module); Json::Value tags; diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/QueryRetrieveHandler.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/QueryRetrieveHandler.cpp Wed May 27 12:32:43 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 . + **/ + + +#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); + } + } +} diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/QueryRetrieveHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/QueryRetrieveHandler.h Wed May 27 12:32:43 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 . + **/ + + +#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); + }; +} diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/ServerContext.cpp Wed May 27 12:32:43 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")); diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/ServerContext.h Wed May 27 12:32:43 2015 +0200 @@ -43,6 +43,7 @@ #include "Scheduler/ServerScheduler.h" #include "DicomInstanceToStore.h" #include "ServerIndexChange.h" +#include "../Core/Cache/SharedArchive.h" #include @@ -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_; + } }; } diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/ServerIndex.cpp Wed May 27 12:32:43 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, diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/ServerIndex.h diff -r 4460e2622016 -r 7b6f5115607f OrthancServer/main.cpp --- a/OrthancServer/main.cpp Wed May 27 10:50:59 2015 +0200 +++ b/OrthancServer/main.cpp Wed May 27 12:32:43 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(); diff -r 4460e2622016 -r 7b6f5115607f Resources/Configuration.json --- a/Resources/Configuration.json Wed May 27 10:50:59 2015 +0200 +++ b/Resources/Configuration.json Wed May 27 12:32:43 2015 +0200 @@ -222,5 +222,10 @@ // are issued. This option sets the number of seconds of inactivity // to wait before automatically closing a DICOM association. If set // to 0, the connection is closed immediately. - "DicomAssociationCloseDelay" : 5 + "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 } diff -r 4460e2622016 -r 7b6f5115607f UnitTestsSources/DicomMapTests.cpp --- a/UnitTestsSources/DicomMapTests.cpp Wed May 27 10:50:59 2015 +0200 +++ b/UnitTestsSources/DicomMapTests.cpp Wed May 27 12:32:43 2015 +0200 @@ -153,7 +153,7 @@ DicomModule module) { std::set moduleTags, main; - DicomTag::GetTagsForModule(moduleTags, module); + DicomTag::AddTagsForModule(moduleTags, module); DicomMap::GetMainDicomTags(main, level); // The main dicom tags are a subset of the module diff -r 4460e2622016 -r 7b6f5115607f UnitTestsSources/MemoryCacheTests.cpp --- a/UnitTestsSources/MemoryCacheTests.cpp Wed May 27 10:50:59 2015 +0200 +++ b/UnitTestsSources/MemoryCacheTests.cpp Wed May 27 12:32:43 2015 +0200 @@ -39,6 +39,7 @@ #include #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(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(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 i; + a.List(i); + + size_t count = 0; + for (std::list::const_iterator + it = i.begin(); it != i.end(); it++) + { + if (*it == first || + *it == second) + { + count++; + } + } + + ASSERT_EQ(2, count); +}