Mercurial > hg > orthanc-dicomweb
changeset 105:e1e2b6b2139d dev
test with DICOMweb STOW-RS client
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 26 Apr 2016 17:38:47 +0200 |
parents | 4274441e21d4 |
children | 100c20770a25 |
files | NEWS Plugin/Configuration.cpp Plugin/Configuration.h Plugin/Plugin.cpp Plugin/StowRs.cpp |
diffstat | 5 files changed, 386 insertions(+), 38 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Mon Apr 25 13:29:40 2016 +0200 +++ b/NEWS Tue Apr 26 17:38:47 2016 +0200 @@ -1,6 +1,11 @@ Pending changes in the mainline =============================== +=> Minimum SDK version: 1.0.1 <= + +* STOW-RS client +* Better robustness in the STOW-RS server + Version 0.2 (2015/12/10) ========================
--- a/Plugin/Configuration.cpp Mon Apr 25 13:29:40 2016 +0200 +++ b/Plugin/Configuration.cpp Tue Apr 26 17:38:47 2016 +0200 @@ -23,6 +23,7 @@ #include <fstream> #include <json/reader.h> #include <boost/regex.hpp> +#include <boost/lexical_cast.hpp> #include "../Orthanc/Core/Toolbox.h" @@ -81,48 +82,108 @@ void ParseMultipartBody(std::vector<MultipartItem>& result, + OrthancPluginContext* context, const char* body, const uint64_t bodySize, const std::string& boundary) { + // Reference: + // https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + result.clear(); - boost::regex header("\r?(\n?)--" + boundary + "(--|.*\r?\n\r?\n)"); - boost::regex pattern(".*^Content-Type\\s*:\\s*([^\\s]*).*", - boost::regex::icase /* case insensitive */); + const boost::regex separator("\r\n--" + boundary + "(--|\r\n)"); + const boost::regex encapsulation("(.*)\r\n\r\n(.*)"); + + std::vector< std::pair<const char*, const char*> > parts; - boost::cmatch what; - boost::match_flag_type flags = (boost::match_perl | - boost::match_not_dot_null); const char* start = body; const char* end = body + bodySize; - std::string currentType; - while (boost::regex_search(start, end, what, header, flags)) + boost::cmatch what; + boost::match_flag_type flags = boost::match_perl; + while (boost::regex_search(start, end, what, separator, flags)) { - if (start != body) + if (start != body) // Ignore the first separator { - MultipartItem item; - item.data_ = start; - item.size_ = what[0].first - start; - item.contentType_ = currentType; + parts.push_back(std::make_pair(start, what[0].first)); + } - result.push_back(item); + if (*what[1].first == '-') + { + // This is the last separator (there is a trailing "--") + break; } - boost::cmatch contentType; - if (boost::regex_match(what[0].first, what[0].second, contentType, pattern)) - { - currentType = contentType[1]; - } - else - { - currentType.clear(); - } - start = what[0].second; flags |= boost::match_prev_avail; } + + + for (size_t i = 0; i < parts.size(); i++) + { + if (boost::regex_match(parts[i].first, parts[i].second, what, encapsulation, boost::match_perl)) + { + size_t dicomSize = what[2].second - what[2].first; + + std::string contentType = "application/octet-stream"; + std::vector<std::string> headers; + + { + std::string tmp; + tmp.assign(what[1].first, what[1].second); + Orthanc::Toolbox::TokenizeString(headers, tmp, '\n'); + } + + bool valid = true; + + for (size_t j = 0; j < headers.size(); j++) + { + std::vector<std::string> tokens; + Orthanc::Toolbox::TokenizeString(tokens, headers[j], ':'); + + if (tokens.size() == 2) + { + std::string key = Orthanc::Toolbox::StripSpaces(tokens[0]); + std::string value = Orthanc::Toolbox::StripSpaces(tokens[1]); + Orthanc::Toolbox::ToLowerCase(key); + + if (key == "content-type") + { + contentType = value; + } + else if (key == "content-length") + { + try + { + size_t s = boost::lexical_cast<size_t>(value); + if (s != dicomSize) + { + valid = false; + } + } + catch (boost::bad_lexical_cast&) + { + valid = false; + } + } + } + } + + if (valid) + { + MultipartItem item; + item.data_ = what[2].first; + item.size_ = dicomSize; + item.contentType_ = contentType; + result.push_back(item); + } + else + { + OrthancPluginLogWarning(context, "Ignoring a badly-formatted item in a multipart body"); + } + } + } } @@ -132,7 +193,7 @@ bool applyPlugins) { OrthancPluginMemoryBuffer buffer; - int code; + OrthancPluginErrorCode code; if (applyPlugins) { @@ -143,7 +204,7 @@ code = OrthancPluginRestApiGet(context, &buffer, uri.c_str()); } - if (code) + if (code != OrthancPluginErrorCode_Success) { // Error return false; @@ -179,10 +240,15 @@ bool applyPlugins) { std::string content; - RestApiGetString(content, context, uri, applyPlugins); - - Json::Reader reader; - return reader.parse(content, result); + if (!RestApiGetString(content, context, uri, applyPlugins)) + { + return false; + } + else + { + Json::Reader reader; + return reader.parse(content, result); + } } @@ -192,9 +258,9 @@ const std::string& body) { OrthancPluginMemoryBuffer buffer; - int code = OrthancPluginRestApiPost(context, &buffer, uri.c_str(), body.c_str(), body.size()); + OrthancPluginErrorCode code = OrthancPluginRestApiPost(context, &buffer, uri.c_str(), body.c_str(), body.size()); - if (code) + if (code != OrthancPluginErrorCode_Success) { // Error return false; @@ -230,10 +296,15 @@ const std::string& body) { std::string content; - RestApiPostString(content, context, uri, body); - - Json::Reader reader; - return reader.parse(content, result); + if (!RestApiPostString(content, context, uri, body)) + { + return false; + } + else + { + Json::Reader reader; + return reader.parse(content, result); + } }
--- a/Plugin/Configuration.h Mon Apr 25 13:29:40 2016 +0200 +++ b/Plugin/Configuration.h Tue Apr 26 17:38:47 2016 +0200 @@ -49,6 +49,7 @@ const std::string& header); void ParseMultipartBody(std::vector<MultipartItem>& result, + OrthancPluginContext* context, const char* body, const uint64_t bodySize, const std::string& boundary);
--- a/Plugin/Plugin.cpp Mon Apr 25 13:29:40 2016 +0200 +++ b/Plugin/Plugin.cpp Tue Apr 26 17:38:47 2016 +0200 @@ -33,6 +33,12 @@ #include <gdcmGlobal.h> +#include <json/reader.h> +#include <list> +#include "../Orthanc/Core/ChunkedBuffer.h" +#include "../Orthanc/Core/Toolbox.h" + + // Global state OrthancPluginContext* context_ = NULL; Json::Value configuration_; @@ -133,6 +139,270 @@ + + + +static void AddInstance(std::list<std::string>& target, + const Json::Value& instance) +{ + if (instance.type() != Json::objectValue || + !instance.isMember("ID") || + instance["ID"].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + target.push_back(instance["ID"].asString()); + } +} + + + + +static bool GetSequenceSize(size_t& result, + const Json::Value& answer, + const std::string& tag, + bool isMandatory, + const std::string& peer) +{ + const Json::Value* value = NULL; + + std::string upper, lower; + Orthanc::Toolbox::ToUpperCase(upper, tag); + Orthanc::Toolbox::ToLowerCase(lower, tag); + + if (answer.isMember(upper)) + { + value = &answer[upper]; + } + else if (answer.isMember(lower)) + { + value = &answer[lower]; + } + else if (isMandatory) + { + std::string s = ("The STOW-RS JSON response from DICOMweb peer " + peer + + " does not contain the mandatory tag " + upper); + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + else + { + return false; + } + + if (value->type() != Json::objectValue || + !value->isMember("Value") || + (*value) ["Value"].type() != Json::arrayValue) + { + std::string s = "Unable to parse STOW-RS JSON response from DICOMweb peer " + peer; + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + result = (*value) ["Value"].size(); + return true; +} + + +static void SendStowRequest(const std::string& url, + const char* username, + const char* password, + const std::string& body, + const std::string& mime, + size_t countInstances) +{ + const char* headersKeys[] = { + "Accept", + "Expect", + "Content-Type" + }; + + const char* headersValues[] = { + "application/json", + "", + mime.c_str() + }; + + uint16_t status = 0; + OrthancPluginMemoryBuffer answer; + OrthancPluginErrorCode code = OrthancPluginHttpClient(context_, &answer, &status, OrthancPluginHttpMethod_Post, + url.c_str(), 3, headersKeys, headersValues, + body.c_str(), body.size(), username, password, 0); + if (code != OrthancPluginErrorCode_Success || + (status != 200 && status != 202)) + { + std::string s = ("Cannot send DICOM images through STOW-RS to DICOMweb peer " + url + + " (HTTP status: " + boost::lexical_cast<std::string>(status) + ")"); + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(static_cast<Orthanc::ErrorCode>(code)); + } + + Json::Value response; + Json::Reader reader; + bool success = reader.parse(reinterpret_cast<const char*>(answer.data), + reinterpret_cast<const char*>(answer.data) + answer.size, response); + OrthancPluginFreeMemoryBuffer(context_, &answer); + + if (!success || + response.type() != Json::objectValue || + !response.isMember("00081199")) + { + std::string s = "Unable to parse STOW-RS JSON response from DICOMweb peer " + url; + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + size_t size; + if (!GetSequenceSize(size, response, "00081199", true, url) || + size != countInstances) + { + std::string s = ("The STOW-RS server was only able to receive " + + boost::lexical_cast<std::string>(size) + " instances out of " + + boost::lexical_cast<std::string>(countInstances)); + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + if (GetSequenceSize(size, response, "00081198", false, url) && + size != 0) + { + std::string s = ("The response from the STOW-RS server contains " + + boost::lexical_cast<std::string>(size) + + " items in its Failed SOP Sequence (0008,1198) tag"); + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + if (GetSequenceSize(size, response, "0008119A", false, url) && + size != 0) + { + std::string s = ("The response from the STOW-RS server contains " + + boost::lexical_cast<std::string>(size) + + " items in its Other Failures Sequence (0008,119A) tag"); + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } +} + + + +void StowClient(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->groupsCount != 1) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest); + } + + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(context_, output, "POST"); + return; + } + + std::string peer(request->groups[0]); + + Json::Value resources; + Json::Reader reader; + if (!reader.parse(request->body, request->body + request->bodySize, resources) || + resources.type() != Json::arrayValue) + { + std::string s = "The list of resources to be sent through DICOMweb STOW-RS must be given as a JSON array"; + OrthancPluginLogError(context_, s.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + // Extract information about all the child instances + std::list<std::string> instances; + for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++) + { + if (resources[i].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + std::string resource = resources[i].asString(); + + Json::Value tmp; + if (OrthancPlugins::RestApiGetJson(tmp, context_, "/instances/" + resource, false)) + { + AddInstance(instances, tmp); + } + else if ((OrthancPlugins::RestApiGetJson(tmp, context_, "/series/" + resource, false) && + OrthancPlugins::RestApiGetJson(tmp, context_, "/series/" + resource + "/instances", false)) || + (OrthancPlugins::RestApiGetJson(tmp, context_, "/studies/" + resource, false) && + OrthancPlugins::RestApiGetJson(tmp, context_, "/studies/" + resource + "/instances", false)) || + (OrthancPlugins::RestApiGetJson(tmp, context_, "/patients/" + resource, false) && + OrthancPlugins::RestApiGetJson(tmp, context_, "/patients/" + resource + "/instances", false))) + { + if (tmp.type() != Json::arrayValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + for (Json::Value::ArrayIndex j = 0; j < tmp.size(); j++) + { + AddInstance(instances, tmp[j]); + } + } + else + { + // Unkown resource + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + } + + + std::string boundary; + + { + char* uuid = OrthancPluginGenerateUuid(context_); + try + { + boundary.assign(uuid); + } + catch (...) + { + OrthancPluginFreeString(context_, uuid); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory); + } + + OrthancPluginFreeString(context_, uuid); + } + + std::string mime = "multipart/related; type=application/dicom; boundary=" + boundary; + + Orthanc::ChunkedBuffer chunks; + chunks.AddChunk("\r\n"); // Empty preamble + + for (std::list<std::string>::const_iterator it = instances.begin(); it != instances.end(); it++) + { + std::string dicom; + if (OrthancPlugins::RestApiGetString(dicom, context_, "/instances/" + *it + "/file")) + { + chunks.AddChunk("--" + boundary + "\r\n" + + "Content-Type: application/dicom\r\n" + + "Content-Length: " + boost::lexical_cast<std::string>(dicom.size()) + + "\r\n\r\n"); + chunks.AddChunk(dicom); + chunks.AddChunk("\r\n"); + } + } + + chunks.AddChunk("--" + boundary + "--\r\n"); + + std::string body; + chunks.Flatten(body); + + // TODO Split the message + + SendStowRequest("http://localhost:8043/dicom-web/studies", NULL, NULL, body, mime, instances.size()); +} + + extern "C" { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) @@ -208,6 +478,8 @@ Register(root, "studies/([^/]*)/series/([^/]*)/metadata", Protect<RetrieveSeriesMetadata>); Register(root, "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames", Protect<RetrieveFrames>); Register(root, "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames/([^/]*)", Protect<RetrieveFrames>); + + Register(root, "peers/([^/]*)/stow", Protect<StowClient>); } else {
--- a/Plugin/StowRs.cpp Mon Apr 25 13:29:40 2016 +0200 +++ b/Plugin/StowRs.cpp Tue Apr 26 17:38:47 2016 +0200 @@ -147,14 +147,13 @@ } - bool isFirst = true; gdcm::DataSet result; gdcm::SmartPointer<gdcm::SequenceOfItems> success = new gdcm::SequenceOfItems(); gdcm::SmartPointer<gdcm::SequenceOfItems> failed = new gdcm::SequenceOfItems(); std::vector<OrthancPlugins::MultipartItem> items; - OrthancPlugins::ParseMultipartBody(items, request->body, request->bodySize, boundary); + OrthancPlugins::ParseMultipartBody(items, context_, request->body, request->bodySize, boundary); for (size_t i = 0; i < items.size(); i++) { if (!items[i].contentType_.empty() &&