Mercurial > hg > orthanc
changeset 1368:b22ba8c5edbe query-retrieve
query retrieve
line wrap: on
line diff
--- a/CMakeLists.txt Fri May 22 17:40:10 2015 +0200 +++ b/CMakeLists.txt Tue May 26 17:54:34 2015 +0200 @@ -173,6 +173,7 @@ OrthancServer/ExportedResource.cpp OrthancServer/ResourceFinder.cpp OrthancServer/DicomFindQuery.cpp + OrthancServer/QueryRetrieveHandler.cpp # From "lua-scripting" branch OrthancServer/DicomInstanceToStore.cpp
--- a/Core/Cache/SharedArchive.cpp Fri May 22 17:40:10 2015 +0200 +++ b/Core/Cache/SharedArchive.cpp Tue May 26 17:54:34 2015 +0200 @@ -69,6 +69,16 @@ } + SharedArchive::SharedArchive(size_t maxSize) : + maxSize_(maxSize) + { + if (maxSize == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + SharedArchive::~SharedArchive() { for (Archive::iterator it = archive_.begin();
--- a/Core/Cache/SharedArchive.h Fri May 22 17:40:10 2015 +0200 +++ b/Core/Cache/SharedArchive.h Tue May 26 17:54:34 2015 +0200 @@ -70,9 +70,7 @@ }; - SharedArchive(size_t maxSize) : maxSize_(maxSize) - { - } + SharedArchive(size_t maxSize); ~SharedArchive();
--- a/Core/DicomFormat/DicomTag.cpp Fri May 22 17:40:10 2015 +0200 +++ b/Core/DicomFormat/DicomTag.cpp Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/Core/DicomFormat/DicomTag.h Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/Core/RestApi/RestApi.cpp Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/Core/RestApi/RestApiCall.cpp Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/Core/RestApi/RestApiCall.h Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/Core/RestApi/RestApiOutput.cpp Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/Core/RestApi/RestApiOutput.h Tue May 26 17:54:34 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/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Fri May 22 17:40:10 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Tue May 26 17:54:34 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> @@ -372,10 +373,51 @@ } } + + 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); + } + + 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; @@ -383,58 +425,27 @@ 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) @@ -450,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), ""); @@ -459,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; @@ -504,59 +532,6 @@ } - 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, - 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::MoveInternal(const std::string& targetAet, const DicomMap& fields) {
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h Fri May 22 17:40:10 2015 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.h Tue May 26 17:54:34 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,10 +64,6 @@ void SetupPresentationContexts(const std::string& preferredTransferSyntax); - void Find(DicomFindAnswers& result, - FindRootModel model, - const DicomMap& fields); - void MoveInternal(const std::string& targetAet, const DicomMap& fields); @@ -150,17 +138,9 @@ void StoreFile(const std::string& path); - void FindPatient(DicomFindAnswers& result, - const DicomMap& fields); - - void FindStudy(DicomFindAnswers& result, - const DicomMap& fields); - - void FindSeries(DicomFindAnswers& result, - const DicomMap& fields); - - void FindInstance(DicomFindAnswers& result, - const DicomMap& fields); + void Find(DicomFindAnswers& result, + ResourceType level, + const DicomMap& fields); void Move(const std::string& targetAet, const DicomMap& findResult);
--- a/OrthancServer/FromDcmtkBridge.cpp Fri May 22 17:40:10 2015 +0200 +++ b/OrthancServer/FromDcmtkBridge.cpp Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/OrthancServer/FromDcmtkBridge.h Tue May 26 17:54:34 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/OrthancRestApi/OrthancRestModalities.cpp Fri May 22 17:40:10 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Tue May 26 17:54:34 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 Tue May 26 17:54:34 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 Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/OrthancServer/ServerContext.cpp Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/OrthancServer/ServerContext.h Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/OrthancServer/ServerIndex.cpp Tue May 26 17:54:34 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 22 17:40:10 2015 +0200 +++ b/OrthancServer/main.cpp Tue May 26 17:54:34 2015 +0200 @@ -384,6 +384,8 @@ + + static bool StartOrthanc(int argc, char *argv[]) { #if ENABLE_PLUGINS == 1 @@ -533,38 +535,6 @@ LOG(WARNING) << "Orthanc has started"; - - if (1) - { - DicomUserConnection c; - c.SetLocalApplicationEntityTitle("ORTHANC"); - c.SetRemoteApplicationEntityTitle("ORTHANC"); - c.SetRemoteHost("localhost"); - c.SetRemotePort(4343); - c.Open(); - - DicomMap m; // Cardiac - m.SetValue(DICOM_TAG_PATIENT_ID, "3390592L"); - //m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "1.3.51.0.1.1.192.168.29.133.1681753.1681732"); - //m.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "1.3.12.2.1107.5.2.33.37097.2012041612474981424569674.0.0.0"); - //m.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "1.3.12.2.1107.5.2.33.37097.2012041612485535037669708"); - - DicomFindAnswers fnd; - c.FindPatient(fnd, m); - //c.FindInstance(fnd, m); - //c.FindSeries(fnd, m); - //c.FindStudy(fnd, m); - - for (size_t i = 0; i < fnd.GetSize(); i++) - { - FromDcmtkBridge::Print(stdout, fnd.GetAnswer(i)); - c.Move("ORTHANC", fnd.GetAnswer(i)); - } - - printf("ok %d\n", fnd.GetSize()); - } - - Toolbox::ServerBarrier(restApi.ResetRequestReceivedFlag()); isReset = restApi.ResetRequestReceivedFlag();
--- a/Resources/Configuration.json Fri May 22 17:40:10 2015 +0200 +++ b/Resources/Configuration.json Tue May 26 17:54:34 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 }
--- a/UnitTestsSources/DicomMapTests.cpp Fri May 22 17:40:10 2015 +0200 +++ b/UnitTestsSources/DicomMapTests.cpp Tue May 26 17:54:34 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