# HG changeset patch # User Sebastien Jodogne # Date 1466760896 -7200 # Node ID f5d135633484e76edf1ade7921704a1968d05f7c # Parent 1aad2bf98d8ce9887859832a2595c212b59a4a49 Retrieval of DICOM instances with WADO-RS through URI "/{dicom-web}/servers/{id}/retrieve" diff -r 1aad2bf98d8c -r f5d135633484 NEWS --- a/NEWS Fri Jun 24 10:12:25 2016 +0200 +++ b/NEWS Fri Jun 24 11:34:56 2016 +0200 @@ -5,6 +5,7 @@ * STOW-RS client with URI "/{dicom-web}/servers/{id}/stow" * QIDO-RS and WADO-RS client with URI "/{dicom-web}/servers/{id}/get" +* Retrieval of DICOM instances with WADO-RS through URI "/{dicom-web}/servers/{id}/retrieve" * Improved robustness in the STOW-RS server * Fix issue #13 (QIDO-RS study-level query is slow) * Fix issue #14 (Aggregate fields empty for QIDO-RS study/series-level queries) diff -r 1aad2bf98d8c -r f5d135633484 Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri Jun 24 10:12:25 2016 +0200 +++ b/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri Jun 24 11:34:56 2016 +0200 @@ -135,7 +135,8 @@ bool MemoryBuffer::RestApiPost(const std::string& uri, - const std::string& body, + const char* body, + size_t bodySize, bool applyPlugins) { Clear(); @@ -144,11 +145,11 @@ if (applyPlugins) { - error = OrthancPluginRestApiPostAfterPlugins(context_, &buffer_, uri.c_str(), body.c_str(), body.size()); + error = OrthancPluginRestApiPostAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize); } else { - error = OrthancPluginRestApiPost(context_, &buffer_, uri.c_str(), body.c_str(), body.size()); + error = OrthancPluginRestApiPost(context_, &buffer_, uri.c_str(), body, bodySize); } if (error == OrthancPluginErrorCode_Success) @@ -167,7 +168,8 @@ bool MemoryBuffer::RestApiPut(const std::string& uri, - const std::string& body, + const char* body, + size_t bodySize, bool applyPlugins) { Clear(); @@ -176,11 +178,11 @@ if (applyPlugins) { - error = OrthancPluginRestApiPutAfterPlugins(context_, &buffer_, uri.c_str(), body.c_str(), body.size()); + error = OrthancPluginRestApiPutAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize); } else { - error = OrthancPluginRestApiPut(context_, &buffer_, uri.c_str(), body.c_str(), body.size()); + error = OrthancPluginRestApiPut(context_, &buffer_, uri.c_str(), body, bodySize); } if (error == OrthancPluginErrorCode_Success) diff -r 1aad2bf98d8c -r f5d135633484 Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h --- a/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h Fri Jun 24 10:12:25 2016 +0200 +++ b/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h Fri Jun 24 11:34:56 2016 +0200 @@ -107,11 +107,13 @@ bool applyPlugins); bool RestApiPost(const std::string& uri, - const std::string& body, + const char* body, + size_t bodySize, bool applyPlugins); bool RestApiPut(const std::string& uri, - const std::string& body, + const char* body, + size_t bodySize, bool applyPlugins); }; diff -r 1aad2bf98d8c -r f5d135633484 Plugin/Configuration.h --- a/Plugin/Configuration.h Fri Jun 24 10:12:25 2016 +0200 +++ b/Plugin/Configuration.h Fri Jun 24 11:34:56 2016 +0200 @@ -33,6 +33,7 @@ namespace OrthancPlugins { + // TODO MOVE THIS to DicomWebServers struct MultipartItem { const char* data_; diff -r 1aad2bf98d8c -r f5d135633484 Plugin/Plugin.cpp --- a/Plugin/Plugin.cpp Fri Jun 24 10:12:25 2016 +0200 +++ b/Plugin/Plugin.cpp Fri Jun 24 11:34:56 2016 +0200 @@ -139,6 +139,33 @@ } + +static bool GetStringValue(std::string& target, + const Json::Value& json, + const std::string& key) +{ + if (json.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else if (!json.isMember(key)) + { + target.clear(); + return false; + } + else if (json[key].type() != Json::stringValue) + { + OrthancPlugins::Configuration::LogError("The field \"" + key + "\" in a JSON object should be a string"); + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + target = json[key].asString(); + return true; + } +} + + void GetFromServer(OrthancPluginRestOutput* output, const char* /*url*/, const OrthancPluginHttpRequest* request) @@ -155,23 +182,23 @@ Orthanc::WebServiceParameters server(OrthancPlugins::DicomWebServers::GetInstance().GetServer(request->groups[0])); + std::string tmp; Json::Value body; Json::Reader reader; if (!reader.parse(request->body, request->body + request->bodySize, body) || body.type() != Json::objectValue || - !body.isMember(URI) || - body[URI].type() != Json::stringValue) + !GetStringValue(tmp, body, URI)) { OrthancPlugins::Configuration::LogError("A request to the DICOMweb STOW-RS client must provide a JSON object " "with the field \"Uri\" containing the URI of interest"); - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat); } std::map getArguments; OrthancPlugins::ParseAssociativeArray(getArguments, body, GET_ARGUMENTS); - std::string uri; - OrthancPlugins::UriEncode(uri, body[URI].asString(), getArguments); + std::string uri; + OrthancPlugins::UriEncode(uri, tmp, getArguments); std::map httpHeaders; OrthancPlugins::ParseAssociativeArray(httpHeaders, body, HTTP_HEADERS); @@ -194,7 +221,7 @@ } else if (key == "transfer-encoding") { - // Do not include this header + // Do not forward this header } else { @@ -209,6 +236,210 @@ +static void RetrieveFromServerInternal(std::set& instances, + const Orthanc::WebServiceParameters& server, + const std::map& httpHeaders, + const Json::Value& resource) +{ + static const std::string STUDY = "Study"; + static const std::string SERIES = "Series"; + static const std::string INSTANCE = "Instance"; + static const std::string MULTIPART_RELATED = "multipart/related"; + static const std::string APPLICATION_DICOM = "application/dicom"; + + if (resource.type() != Json::objectValue) + { + OrthancPlugins::Configuration::LogError("Resources of interest for the DICOMweb WADO-RS Retrieve client " + "must be provided as a JSON object"); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat); + } + + std::string study, series, instance; + if (!GetStringValue(study, resource, STUDY) || + study.empty()) + { + OrthancPlugins::Configuration::LogError("A non-empty \"" + STUDY + "\" field is mandatory for the " + "DICOMweb WADO-RS Retrieve client"); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat); + } + + GetStringValue(series, resource, SERIES); + GetStringValue(instance, resource, INSTANCE); + + if (series.empty() && + !instance.empty()) + { + OrthancPlugins::Configuration::LogError("When specifying a \"" + INSTANCE + "\" field in a call to DICOMweb " + "WADO-RS Retrieve client, the \"" + SERIES + "\" field is mandatory"); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat); + } + + std::string uri = "studies/" + study; + if (!series.empty()) + { + uri += "/" + series; + if (!instance.empty()) + { + uri += "/" + instance; + } + } + + OrthancPlugins::MemoryBuffer answerBody(OrthancPlugins::Configuration::GetContext()); + std::map answerHeaders; + OrthancPlugins::CallServer(answerBody, answerHeaders, server, OrthancPluginHttpMethod_Get, httpHeaders, uri, ""); + + std::vector contentType; + for (std::map::const_iterator + it = answerHeaders.begin(); it != answerHeaders.end(); ++it) + { + std::string s = Orthanc::Toolbox::StripSpaces(it->first); + Orthanc::Toolbox::ToLowerCase(s); + if (s == "content-type") + { + Orthanc::Toolbox::TokenizeString(contentType, it->second, ';'); + break; + } + } + + if (contentType.empty()) + { + OrthancPlugins::Configuration::LogError("No Content-Type provided by the remote WADO-RS server"); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol); + } + + Orthanc::Toolbox::ToLowerCase(contentType[0]); + if (Orthanc::Toolbox::StripSpaces(contentType[0]) != MULTIPART_RELATED) + { + OrthancPlugins::Configuration::LogError("The remote WADO-RS server answers with a \"" + contentType[0] + + "\" Content-Type, but \"" + MULTIPART_RELATED + "\" is expected"); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol); + } + + std::string type, boundary; + for (size_t i = 1; i < contentType.size(); i++) + { + std::vector tokens; + Orthanc::Toolbox::TokenizeString(tokens, contentType[i], '='); + + if (tokens.size() == 2) + { + std::string s = Orthanc::Toolbox::StripSpaces(tokens[0]); + Orthanc::Toolbox::ToLowerCase(s); + + if (s == "type") + { + type = Orthanc::Toolbox::StripSpaces(tokens[1]); + Orthanc::Toolbox::ToLowerCase(type); + } + else if (s == "boundary") + { + boundary = Orthanc::Toolbox::StripSpaces(tokens[1]); + } + } + } + + if (type != APPLICATION_DICOM) + { + OrthancPlugins::Configuration::LogError("The remote WADO-RS server answers with a \"" + type + + "\" multipart Content-Type, but \"" + APPLICATION_DICOM + "\" is expected"); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol); + } + + if (boundary.empty()) + { + OrthancPlugins::Configuration::LogError("The remote WADO-RS server does not provide a boundary for its multipart answer"); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol); + } + + std::vector parts; + OrthancPlugins::ParseMultipartBody(parts, OrthancPlugins::Configuration::GetContext(), + reinterpret_cast(answerBody.GetData()), + answerBody.GetSize(), boundary); + + OrthancPlugins::Configuration::LogInfo("The remote WADO-RS server has provided " + + boost::lexical_cast(parts.size()) + + " DICOM instances"); + + for (size_t i = 0; i < parts.size(); i++) + { + if (parts[i].contentType_ != APPLICATION_DICOM) + { + OrthancPlugins::Configuration::LogError("The remote WADO-RS server has provided a non-DICOM file in its multipart answer"); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol); + } + + OrthancPlugins::MemoryBuffer tmp(OrthancPlugins::Configuration::GetContext()); + tmp.RestApiPost("/instances", parts[i].data_, parts[i].size_, false); + + Json::Value result; + tmp.ToJson(result); + + if (result.type() != Json::objectValue || + !result.isMember("ID") || + result["ID"].type() != Json::stringValue) + { + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError); + } + else + { + instances.insert(result["ID"].asString()); + } + } +} + + + +void RetrieveFromServer(OrthancPluginRestOutput* output, + const char* /*url*/, + const OrthancPluginHttpRequest* request) +{ + static const std::string RESOURCES("Resources"); + static const char* HTTP_HEADERS = "HttpHeaders"; + + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::Configuration::GetContext(), output, "POST"); + return; + } + + Orthanc::WebServiceParameters server(OrthancPlugins::DicomWebServers::GetInstance().GetServer(request->groups[0])); + + Json::Value body; + Json::Reader reader; + if (!reader.parse(request->body, request->body + request->bodySize, body) || + body.type() != Json::objectValue || + !body.isMember(RESOURCES) || + body[RESOURCES].type() != Json::arrayValue) + { + OrthancPlugins::Configuration::LogError("A request to the DICOMweb WADO-RS Retrieve client must provide a JSON object " + "with the field \"" + RESOURCES + "\" containing an array of resources"); + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat); + } + + std::map httpHeaders; + OrthancPlugins::ParseAssociativeArray(httpHeaders, body, HTTP_HEADERS); + + std::set instances; + for (Json::Value::ArrayIndex i = 0; i < body[RESOURCES].size(); i++) + { + RetrieveFromServerInternal(instances, server, httpHeaders, body[RESOURCES][i]); + } + + Json::Value status = Json::objectValue; + status["Instances"] = Json::arrayValue; + + for (std::set::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + status["Instances"].append(*it); + } + + std::string s = status.toStyledString(); + OrthancPluginAnswerBuffer(OrthancPlugins::Configuration::GetContext(), output, s.c_str(), s.size(), "application/json"); +} + + + extern "C" { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) @@ -264,6 +495,7 @@ OrthancPlugins::RegisterRestCallback(context, root + "servers/([^/]*)", true); OrthancPlugins::RegisterRestCallback(context, root + "servers/([^/]*)/stow", true); OrthancPlugins::RegisterRestCallback(context, root + "servers/([^/]*)/get", true); + OrthancPlugins::RegisterRestCallback(context, root + "servers/([^/]*)/retrieve", true); } else {