Mercurial > hg > orthanc-dicomweb
changeset 14:1b383403c080
Support of WADO-RS - RetrieveMetadata
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 07 May 2015 17:07:25 +0200 |
parents | e432ee128884 |
children | 0ed8bbf35577 |
files | CMakeLists.txt Core/Dicom.cpp Core/Dicom.h Core/DicomResults.cpp Core/DicomResults.h NEWS Plugin/Plugin.cpp Plugin/QidoRs.cpp Plugin/StowRs.cpp Plugin/WadoRs.cpp Plugin/WadoRs.h Samples/JavaScript/qido-rs.js Status.txt |
diffstat | 13 files changed, 1240 insertions(+), 814 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Thu Apr 30 15:53:07 2015 +0200 +++ b/CMakeLists.txt Thu May 07 17:07:25 2015 +0200 @@ -79,9 +79,10 @@ ${PUGIXML_SOURCES} Core/ChunkedBuffer.cpp Core/Configuration.cpp + Core/Dicom.cpp + Core/DicomResults.cpp + Core/MultipartWriter.cpp Core/Toolbox.cpp - Core/Dicom.cpp - Core/MultipartWriter.cpp ) add_library(OrthancDicomWeb SHARED ${CORE_SOURCES}
--- a/Core/Dicom.cpp Thu Apr 30 15:53:07 2015 +0200 +++ b/Core/Dicom.cpp Thu May 07 17:07:25 2015 +0200 @@ -21,6 +21,7 @@ #include "Dicom.h" #include "ChunkedBuffer.h" +#include "MultipartWriter.h" #include <gdcmDictEntry.h> #include <boost/lexical_cast.hpp> @@ -75,12 +76,11 @@ } - bool ParsedDicomFile::GetTag(std::string& result, - const gdcm::Tag& tag, - bool stripSpaces) const + static bool GetTag(std::string& result, + const gdcm::DataSet& dataset, + const gdcm::Tag& tag, + bool stripSpaces) { - const gdcm::DataSet& dataset = GetDataSet(); - if (dataset.FindDataElement(tag)) { const gdcm::ByteValue* value = dataset.GetDataElement(tag).GetByteValue(); @@ -101,25 +101,42 @@ } - std::string ParsedDicomFile::GetTagWithDefault(const gdcm::Tag& tag, - const std::string& defaultValue, - bool stripSpaces) const + static std::string GetTagWithDefault(const gdcm::DataSet& dataset, + const gdcm::Tag& tag, + const std::string& defaultValue, + bool stripSpaces) { std::string result; - if (!GetTag(result, tag, false)) + if (!GetTag(result, dataset, tag, false)) { result = defaultValue; } if (stripSpaces) { - result = OrthancPlugins::StripSpaces(result); + result = StripSpaces(result); } return result; } + bool ParsedDicomFile::GetTag(std::string& result, + const gdcm::Tag& tag, + bool stripSpaces) const + { + return OrthancPlugins::GetTag(result, GetDataSet(), tag, stripSpaces); + } + + + std::string ParsedDicomFile::GetTagWithDefault(const gdcm::Tag& tag, + const std::string& defaultValue, + bool stripSpaces) const + { + return OrthancPlugins::GetTagWithDefault(GetDataSet(), tag, defaultValue, stripSpaces); + } + + static std::string FormatTag(const gdcm::Tag& tag) { char tmp[16]; @@ -144,7 +161,8 @@ return "RetrieveURL"; } - throw std::runtime_error("Unknown keyword for tag: " + FormatTag(tag)); + //throw std::runtime_error("Unknown keyword for tag: " + FormatTag(tag)); + return NULL; } @@ -166,6 +184,48 @@ } + static bool IsBulkData(const std::string& vr) + { + /** + * Full list of VR (Value Representations) that are admissible for + * being retrieved as bulk data. We commented out some of them, as + * they correspond to strings and not to binary data. + **/ + return (vr == "FL" || + vr == "FD" || + //vr == "IS" || + vr == "LT" || + vr == "OB" || + vr == "OD" || + vr == "OF" || + vr == "OW" || + vr == "SL" || + vr == "SS" || + //vr == "ST" || + vr == "UL" || + vr == "UN" || + vr == "US" || + vr == "UT"); + } + + + static std::string GetBulkUriRoot(const gdcm::DataSet& dicom) + { + std::string study, series, instance; + + if (!GetTag(study, dicom, DICOM_TAG_STUDY_INSTANCE_UID, true) || + !GetTag(series, dicom, DICOM_TAG_SERIES_INSTANCE_UID, true) || + !GetTag(instance, dicom, DICOM_TAG_SOP_INSTANCE_UID, true)) + { + return ""; + } + else + { + return "/wado-rs/studies/" + study + "/series/" + series + "/instances/" + instance + "/bulk/"; + } + } + + static Encoding DetectEncoding(const gdcm::DataSet& dicom) { if (!dicom.FindDataElement(DICOM_TAG_SPECIFIC_CHARACTER_SET)) @@ -217,26 +277,38 @@ static void DicomToXmlInternal(pugi::xml_node& target, const gdcm::Dict& dictionary, const gdcm::DataSet& dicom, - const Encoding sourceEncoding) + const Encoding sourceEncoding, + const std::string& bulkUri) { for (gdcm::DataSet::ConstIterator it = dicom.Begin(); it != dicom.End(); ++it) // "*it" represents a "gdcm::DataElement" { + char path[16]; + sprintf(path, "%04x%04x", it->GetTag().GetGroup(), it->GetTag().GetElement()); + pugi::xml_node node = target.append_child("DicomAttribute"); node.append_attribute("tag").set_value(FormatTag(it->GetTag()).c_str()); - node.append_attribute("keyword").set_value(GetKeyword(dictionary, it->GetTag())); + + const char* keyword = GetKeyword(dictionary, it->GetTag()); + if (keyword != NULL) + { + node.append_attribute("keyword").set_value(keyword); + } bool isSequence = false; + std::string vr; if (it->GetTag() == DICOM_TAG_RETRIEVE_URL) { // The VR of this attribute has changed from UT to UR. - node.append_attribute("vr").set_value("UR"); + vr = "UR"; } else { - node.append_attribute("vr").set_value(GetVRName(isSequence, dictionary, *it)); + vr = GetVRName(isSequence, dictionary, *it); } + node.append_attribute("vr").set_value(vr.c_str()); + if (isSequence) { gdcm::SmartPointer<gdcm::SequenceOfItems> seq = it->GetValueAsSQ(); @@ -246,7 +318,24 @@ pugi::xml_node item = node.append_child("Item"); std::string number = boost::lexical_cast<std::string>(i); item.append_attribute("number").set_value(number.c_str()); - DicomToXmlInternal(item, dictionary, seq->GetItem(i).GetNestedDataSet(), sourceEncoding); + + std::string childUri; + if (!bulkUri.empty()) + { + childUri = bulkUri + std::string(path) + "/" + number + "/"; + } + + DicomToXmlInternal(item, dictionary, seq->GetItem(i).GetNestedDataSet(), sourceEncoding, childUri); + } + } + else if (IsBulkData(vr)) + { + // Bulk data + if (!bulkUri.empty()) + { + pugi::xml_node value = node.append_child("BulkData"); + std::string uri = bulkUri + std::string(path); + value.append_attribute("uri").set_value(uri.c_str()); } } else @@ -265,9 +354,10 @@ } - void DicomToXml(pugi::xml_document& target, - const gdcm::Dict& dictionary, - const gdcm::DataSet& dicom) + static void DicomToXml(pugi::xml_document& target, + const gdcm::Dict& dictionary, + const gdcm::DataSet& dicom, + const std::string& bulkUriRoot) { pugi::xml_node root = target.append_child("NativeDicomModel"); root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM"); @@ -275,7 +365,7 @@ root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance"); Encoding encoding = DetectEncoding(dicom); - DicomToXmlInternal(root, dictionary, dicom, encoding); + DicomToXmlInternal(root, dictionary, dicom, encoding, bulkUriRoot); pugi::xml_node decl = target.prepend_child(pugi::node_declaration); decl.append_attribute("version").set_value("1.0"); @@ -286,6 +376,7 @@ static void DicomToJsonInternal(Json::Value& target, const gdcm::Dict& dictionary, const gdcm::DataSet& dicom, + const std::string& bulkUri, Encoding sourceEncoding) { target = Json::objectValue; @@ -293,19 +384,25 @@ for (gdcm::DataSet::ConstIterator it = dicom.Begin(); it != dicom.End(); ++it) // "*it" represents a "gdcm::DataElement" { + char path[16]; + sprintf(path, "%04x%04x", it->GetTag().GetGroup(), it->GetTag().GetElement()); + Json::Value node = Json::objectValue; bool isSequence = false; + std::string vr; if (it->GetTag() == DICOM_TAG_RETRIEVE_URL) { // The VR of this attribute has changed from UT to UR. - node["vr"] = "UR"; + vr = "UR"; } else { - node["vr"] = GetVRName(isSequence, dictionary, *it); + vr = GetVRName(isSequence, dictionary, *it); } + node["vr"] = vr.c_str(); + if (isSequence) { // Deal with sequences @@ -316,10 +413,26 @@ for (gdcm::SequenceOfItems::SizeType i = 1; i <= seq->GetNumberOfItems(); i++) { Json::Value child; - DicomToJsonInternal(child, dictionary, seq->GetItem(i).GetNestedDataSet(), sourceEncoding); + + std::string childUri; + if (!bulkUri.empty()) + { + std::string number = boost::lexical_cast<std::string>(i); + childUri = bulkUri + std::string(path) + "/" + number + "/"; + } + + DicomToJsonInternal(child, dictionary, seq->GetItem(i).GetNestedDataSet(), childUri, sourceEncoding); node["Value"].append(child); } } + else if (IsBulkData(vr)) + { + // Bulk data + if (!bulkUri.empty()) + { + node["BulkDataURI"] = bulkUri + std::string(path); + } + } else { // Deal with other value representations @@ -337,24 +450,32 @@ } - void DicomToJson(Json::Value& target, - const gdcm::Dict& dictionary, - const gdcm::DataSet& dicom) + static void DicomToJson(Json::Value& target, + const gdcm::Dict& dictionary, + const gdcm::DataSet& dicom, + const std::string& bulkUriRoot) { Encoding encoding = DetectEncoding(dicom); - DicomToJsonInternal(target, dictionary, dicom, encoding); + DicomToJsonInternal(target, dictionary, dicom, bulkUriRoot, encoding); } void GenerateSingleDicomAnswer(std::string& result, const gdcm::Dict& dictionary, const gdcm::DataSet& dicom, - bool isXml) + bool isXml, + bool isBulkAccessible) { + std::string bulkUriRoot; + if (isBulkAccessible) + { + bulkUriRoot = GetBulkUriRoot(dicom); + } + if (isXml) { pugi::xml_document doc; - DicomToXml(doc, dictionary, dicom); + DicomToXml(doc, dictionary, dicom, bulkUriRoot); ChunkedBufferWriter writer; doc.save(writer, " ", pugi::format_default, pugi::encoding_utf8); @@ -364,7 +485,7 @@ else { Json::Value v; - DicomToJson(v, dictionary, dicom); + DicomToJson(v, dictionary, dicom, bulkUriRoot); Json::FastWriter writer; result = writer.write(v); @@ -376,10 +497,11 @@ OrthancPluginRestOutput* output, const gdcm::Dict& dictionary, const gdcm::DataSet& dicom, - bool isXml) + bool isXml, + bool isBulkAccessible) { std::string answer; - GenerateSingleDicomAnswer(answer, dictionary, dicom, isXml); + GenerateSingleDicomAnswer(answer, dictionary, dicom, isXml, isBulkAccessible); OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), isXml ? "application/dicom+xml" : "application/json"); }
--- a/Core/Dicom.h Thu Apr 30 15:53:07 2015 +0200 +++ b/Core/Dicom.h Thu May 07 17:07:25 2015 +0200 @@ -26,6 +26,7 @@ #include <gdcmDataSet.h> #include <pugixml.hpp> #include <gdcmDict.h> +#include <list> namespace OrthancPlugins @@ -74,22 +75,16 @@ }; - void DicomToXml(pugi::xml_document& target, - const gdcm::Dict& dictionary, - const gdcm::DataSet& dicom); - - void DicomToJson(Json::Value& target, - const gdcm::Dict& dictionary, - const gdcm::DataSet& dicom); - void GenerateSingleDicomAnswer(std::string& result, const gdcm::Dict& dictionary, const gdcm::DataSet& dicom, - bool isXml); + bool isXml, + bool isBulkAccessible); void AnswerDicom(OrthancPluginContext* context, OrthancPluginRestOutput* output, const gdcm::Dict& dictionary, const gdcm::DataSet& dicom, - bool isXml); + bool isXml, + bool isBulkAccessible); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomResults.cpp Thu May 07 17:07:25 2015 +0200 @@ -0,0 +1,78 @@ +/** + * 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 Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "DicomResults.h" + +#include "Dicom.h" + +namespace OrthancPlugins +{ + DicomResults::DicomResults(const gdcm::Dict& dictionary, + bool isXml, + bool isBulkAccessible) : + dictionary_(dictionary), + xmlWriter_("application/dicom+xml"), + isFirst_(true), + isXml_(isXml), + isBulkAccessible_(isBulkAccessible) + { + jsonWriter_.AddChunk("[\n"); + } + + void DicomResults::Add(const gdcm::DataSet& dicom) + { + if (isXml_) + { + std::string answer; + GenerateSingleDicomAnswer(answer, dictionary_, dicom, true, isBulkAccessible_); + xmlWriter_.AddPart(answer); + } + else + { + if (!isFirst_) + { + jsonWriter_.AddChunk(",\n"); + } + + std::string item; + GenerateSingleDicomAnswer(item, dictionary_, dicom, false, isBulkAccessible_); + jsonWriter_.AddChunk(item); + } + + isFirst_ = false; + } + + void DicomResults::Answer(OrthancPluginContext* context, + OrthancPluginRestOutput* output) + { + if (isXml_) + { + xmlWriter_.Answer(context, output); + } + else + { + jsonWriter_.AddChunk("]\n"); + + std::string answer; + jsonWriter_.Flatten(answer); + OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json"); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomResults.h Thu May 07 17:07:25 2015 +0200 @@ -0,0 +1,51 @@ +/** + * 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 Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "MultipartWriter.h" +#include "ChunkedBuffer.h" + +#include <gdcmDataSet.h> +#include <gdcmDict.h> + +namespace OrthancPlugins +{ + class DicomResults + { + private: + const gdcm::Dict& dictionary_; + MultipartWriter xmlWriter_; // Used for XML output + ChunkedBuffer jsonWriter_; // Used for JSON output + bool isFirst_; + bool isXml_; + bool isBulkAccessible_; + + public: + DicomResults(const gdcm::Dict& dictionary, + bool isXml, + bool isBulkAccessible); + + void Add(const gdcm::DataSet& dicom); + + void Answer(OrthancPluginContext* context, + OrthancPluginRestOutput* output); + }; +}
--- a/NEWS Thu Apr 30 15:53:07 2015 +0200 +++ b/NEWS Thu May 07 17:07:25 2015 +0200 @@ -3,6 +3,7 @@ No official release yet. Still work in progress. +* Support of WADO-RS - RetrieveMetadata * Support of Visual Studio 2008 * Support of FreeBSD * Support of OS X
--- a/Plugin/Plugin.cpp Thu Apr 30 15:53:07 2015 +0200 +++ b/Plugin/Plugin.cpp Thu May 07 17:07:25 2015 +0200 @@ -74,6 +74,9 @@ OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)", RetrieveDicomStudy); OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/series/([^/]*)", RetrieveDicomSeries); OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/series/([^/]*)/instances/([^/]*)", RetrieveDicomInstance); + OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/metadata", RetrieveStudyMetadata); + OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/series/([^/]*)/metadata", RetrieveSeriesMetadata); + OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/series/([^/]*)/instances/([^/]*)/metadata", RetrieveInstanceMetadata); // STOW-RS callbacks OrthancPluginRegisterRestCallback(context, "/stow-rs/studies", StowCallback);
--- a/Plugin/QidoRs.cpp Thu Apr 30 15:53:07 2015 +0200 +++ b/Plugin/QidoRs.cpp Thu May 07 17:07:25 2015 +0200 @@ -23,6 +23,7 @@ #include "Plugin.h" #include "StowRs.h" // For IsXmlExpected() #include "../Core/Dicom.h" +#include "../Core/DicomResults.h" #include "../Core/Toolbox.h" #include "../Core/Configuration.h" #include "../Core/MultipartWriter.h" @@ -40,768 +41,692 @@ -enum QueryLevel +namespace { - QueryLevel_Study, - QueryLevel_Series, - QueryLevel_Instance -}; + + enum QueryLevel + { + QueryLevel_Study, + QueryLevel_Series, + QueryLevel_Instance + }; -class ModuleMatcher -{ -private: - typedef std::map<gdcm::Tag, std::string> Filters; + class ModuleMatcher + { + private: + typedef std::map<gdcm::Tag, std::string> Filters; - const gdcm::Dict& dictionary_; - bool fuzzy_; - unsigned int offset_; - unsigned int limit_; - std::list<gdcm::Tag> includeFields_; - bool includeAllFields_; - Filters filters_; + const gdcm::Dict& dictionary_; + bool fuzzy_; + unsigned int offset_; + unsigned int limit_; + std::list<gdcm::Tag> includeFields_; + bool includeAllFields_; + Filters filters_; - static inline uint16_t GetCharValue(char c) - { - if (c >= '0' && c <= '9') - return c - '0'; - else if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - else if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - else - return 0; - } + static inline uint16_t GetCharValue(char c) + { + if (c >= '0' && c <= '9') + return c - '0'; + else if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + else + return 0; + } - static inline uint16_t GetTagValue(const char* c) - { - return ((GetCharValue(c[0]) << 12) + - (GetCharValue(c[1]) << 8) + - (GetCharValue(c[2]) << 4) + - GetCharValue(c[3])); - } + static inline uint16_t GetTagValue(const char* c) + { + return ((GetCharValue(c[0]) << 12) + + (GetCharValue(c[1]) << 8) + + (GetCharValue(c[2]) << 4) + + GetCharValue(c[3])); + } - gdcm::Tag ParseTag(const std::string& key) const - { - if (key.size() == 8 && - isxdigit(key[0]) && - isxdigit(key[1]) && - isxdigit(key[2]) && - isxdigit(key[3]) && - isxdigit(key[4]) && - isxdigit(key[5]) && - isxdigit(key[6]) && - isxdigit(key[7])) + gdcm::Tag ParseTag(const std::string& key) const { - return gdcm::Tag(GetTagValue(key.c_str()), - GetTagValue(key.c_str() + 4)); - } - else - { - gdcm::Tag tag; - dictionary_.GetDictEntryByKeyword(key.c_str(), tag); - - if (tag.IsIllegal() || tag.IsPrivate()) + if (key.size() == 8 && + isxdigit(key[0]) && + isxdigit(key[1]) && + isxdigit(key[2]) && + isxdigit(key[3]) && + isxdigit(key[4]) && + isxdigit(key[5]) && + isxdigit(key[6]) && + isxdigit(key[7])) { - if (key.find('.') != std::string::npos) - { - throw std::runtime_error("This QIDO-RS implementation does not support search over sequences: " + key); - } - else - { - throw std::runtime_error("Illegal tag name in QIDO-RS: " + key); - } + return gdcm::Tag(GetTagValue(key.c_str()), + GetTagValue(key.c_str() + 4)); } - - return tag; - } - } - - - static bool IsWildcard(const std::string& constraint) - { - return (constraint.find('-') != std::string::npos || - constraint.find('*') != std::string::npos || - constraint.find('\\') != std::string::npos || - constraint.find('?') != std::string::npos); - } + else + { + gdcm::Tag tag; + dictionary_.GetDictEntryByKeyword(key.c_str(), tag); - static bool ApplyRangeConstraint(const std::string& value, - const std::string& constraint) - { - size_t separator = constraint.find('-'); - std::string lower(constraint.substr(0, separator)); - std::string upper(constraint.substr(separator + 1)); - std::string v(value); - - OrthancPlugins::ToLowerCase(lower); - OrthancPlugins::ToLowerCase(upper); - OrthancPlugins::ToLowerCase(v); - - if (lower.size() == 0 && upper.size() == 0) - { - return false; - } - - if (lower.size() == 0) - { - return v <= upper; - } + if (tag.IsIllegal() || tag.IsPrivate()) + { + if (key.find('.') != std::string::npos) + { + throw std::runtime_error("This QIDO-RS implementation does not support search over sequences: " + key); + } + else + { + throw std::runtime_error("Illegal tag name in QIDO-RS: " + key); + } + } - if (upper.size() == 0) - { - return v >= lower; - } - - return (v >= lower && v <= upper); - } - - - static bool ApplyListConstraint(const std::string& value, - const std::string& constraint) - { - std::string v1(value); - OrthancPlugins::ToLowerCase(v1); - - std::vector<std::string> items; - OrthancPlugins::TokenizeString(items, constraint, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - std::string lower(items[i]); - OrthancPlugins::ToLowerCase(lower); - if (lower == v1) - { - return true; + return tag; } } - return false; - } - - static std::string WildcardToRegularExpression(const std::string& source) - { - std::string result = source; - - // Escape all special characters - boost::replace_all(result, "\\", "\\\\"); - boost::replace_all(result, "^", "\\^"); - boost::replace_all(result, ".", "\\."); - boost::replace_all(result, "$", "\\$"); - boost::replace_all(result, "|", "\\|"); - boost::replace_all(result, "(", "\\("); - boost::replace_all(result, ")", "\\)"); - boost::replace_all(result, "[", "\\["); - boost::replace_all(result, "]", "\\]"); - boost::replace_all(result, "+", "\\+"); - boost::replace_all(result, "/", "\\/"); - boost::replace_all(result, "{", "\\{"); - boost::replace_all(result, "}", "\\}"); - - // Convert wildcards '*' and '?' to their regex equivalents - boost::replace_all(result, "?", "."); - boost::replace_all(result, "*", ".*"); - - return result; - } - - - static bool Matches(const std::string& value, - const std::string& constraint) - { - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained - // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html - - if (constraint.find('-') != std::string::npos) + static bool IsWildcard(const std::string& constraint) { - return ApplyRangeConstraint(value, constraint); - } - - if (constraint.find('\\') != std::string::npos) - { - return ApplyListConstraint(value, constraint); + return (constraint.find('-') != std::string::npos || + constraint.find('*') != std::string::npos || + constraint.find('\\') != std::string::npos || + constraint.find('?') != std::string::npos); } - if (constraint.find('*') != std::string::npos || - constraint.find('?') != std::string::npos) - { - boost::regex pattern(WildcardToRegularExpression(constraint), - boost::regex::icase /* case insensitive search */); - return boost::regex_match(value, pattern); - } - else - { - std::string v(value), c(constraint); - OrthancPlugins::ToLowerCase(v); - OrthancPlugins::ToLowerCase(c); - return v == c; - } - } - - - - static void AddResultAttributesForLevel(std::list<gdcm::Tag>& result, - QueryLevel level) - { - switch (level) + static bool ApplyRangeConstraint(const std::string& value, + const std::string& constraint) { - case QueryLevel_Study: - // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2 - result.push_back(gdcm::Tag(0x0008, 0x0005)); // Specific Character Set - result.push_back(gdcm::Tag(0x0008, 0x0020)); // Study Date - result.push_back(gdcm::Tag(0x0008, 0x0030)); // Study Time - result.push_back(gdcm::Tag(0x0008, 0x0050)); // Accession Number - result.push_back(gdcm::Tag(0x0008, 0x0056)); // Instance Availability - result.push_back(gdcm::Tag(0x0008, 0x0061)); // Modalities in Study - result.push_back(gdcm::Tag(0x0008, 0x0090)); // Referring Physician's Name - result.push_back(gdcm::Tag(0x0008, 0x0201)); // Timezone Offset From UTC - //result.push_back(gdcm::Tag(0x0008, 0x1190)); // Retrieve URL => SPECIAL CASE - result.push_back(gdcm::Tag(0x0010, 0x0010)); // Patient's Name - result.push_back(gdcm::Tag(0x0010, 0x0020)); // Patient ID - result.push_back(gdcm::Tag(0x0010, 0x0030)); // Patient's Birth Date - result.push_back(gdcm::Tag(0x0010, 0x0040)); // Patient's Sex - result.push_back(gdcm::Tag(0x0020, 0x000D)); // Study Instance UID - result.push_back(gdcm::Tag(0x0020, 0x0010)); // Study ID - result.push_back(gdcm::Tag(0x0020, 0x1206)); // Number of Study Related Series - result.push_back(gdcm::Tag(0x0020, 0x1208)); // Number of Study Related Instances - break; - - case QueryLevel_Series: - // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2a - result.push_back(gdcm::Tag(0x0008, 0x0005)); // Specific Character Set - result.push_back(gdcm::Tag(0x0008, 0x0056)); // Modality - result.push_back(gdcm::Tag(0x0008, 0x0201)); // Timezone Offset From UTC - result.push_back(gdcm::Tag(0x0008, 0x103E)); // Series Description - //result.push_back(gdcm::Tag(0x0008, 0x1190)); // Retrieve URL => SPECIAL CASE - result.push_back(gdcm::Tag(0x0020, 0x000E)); // Series Instance UID - result.push_back(gdcm::Tag(0x0020, 0x0011)); // Series Number - result.push_back(gdcm::Tag(0x0020, 0x1209)); // Number of Series Related Instances - result.push_back(gdcm::Tag(0x0040, 0x0244)); // Performed Procedure Step Start Date - result.push_back(gdcm::Tag(0x0040, 0x0245)); // Performed Procedure Step Start Time - result.push_back(gdcm::Tag(0x0040, 0x0275)); // Request Attribute Sequence - break; - - case QueryLevel_Instance: - // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2b - result.push_back(gdcm::Tag(0x0008, 0x0005)); // Specific Character Set - result.push_back(gdcm::Tag(0x0008, 0x0016)); // SOP Class UID - result.push_back(gdcm::Tag(0x0008, 0x0018)); // SOP Instance UID - result.push_back(gdcm::Tag(0x0008, 0x0056)); // Instance Availability - result.push_back(gdcm::Tag(0x0008, 0x0201)); // Timezone Offset From UTC - result.push_back(gdcm::Tag(0x0008, 0x1190)); // Retrieve URL - result.push_back(gdcm::Tag(0x0020, 0x0013)); // Instance Number - result.push_back(gdcm::Tag(0x0028, 0x0010)); // Rows - result.push_back(gdcm::Tag(0x0028, 0x0011)); // Columns - result.push_back(gdcm::Tag(0x0028, 0x0100)); // Bits Allocated - result.push_back(gdcm::Tag(0x0028, 0x0008)); // Number of Frames - break; - - default: - throw std::runtime_error("Internal error"); - } - } - - + size_t separator = constraint.find('-'); + std::string lower(constraint.substr(0, separator)); + std::string upper(constraint.substr(separator + 1)); + std::string v(value); -public: - ModuleMatcher(const OrthancPluginHttpRequest* request) : - dictionary_(gdcm::Global::GetInstance().GetDicts().GetPublicDict()), - fuzzy_(false), - offset_(0), - limit_(0), - includeAllFields_(false) - { - for (int32_t i = 0; i < request->getCount; i++) - { - std::string key(request->getKeys[i]); - std::string value(request->getValues[i]); + OrthancPlugins::ToLowerCase(lower); + OrthancPlugins::ToLowerCase(upper); + OrthancPlugins::ToLowerCase(v); - if (key == "limit") - { - limit_ = boost::lexical_cast<unsigned int>(value); - } - else if (key == "offset") - { - offset_ = boost::lexical_cast<unsigned int>(value); - } - else if (key == "fuzzymatching") - { - if (value == "true") - { - fuzzy_ = true; - } - else if (value == "false") - { - fuzzy_ = false; - } - else - { - throw std::runtime_error("Not a proper value for fuzzy matching (true or false): " + value); - } - } - else if (key == "includefield") - { - if (key == "all") - { - includeAllFields_ = true; - } - else - { - includeFields_.push_back(ParseTag(key)); - } - } - else - { - filters_[ParseTag(key)] = value; - } - } - } - - unsigned int GetLimit() const - { - return limit_; - } - - unsigned int GetOffset() const - { - return offset_; - } - - void AddFilter(const gdcm::Tag& tag, - const std::string& constraint) - { - filters_[tag] = constraint; - } - - bool LookupExactFilter(std::string& constraint, - const gdcm::Tag& tag) const - { - Filters::const_iterator it = filters_.find(tag); - if (it != filters_.end() && - !IsWildcard(it->second)) - { - constraint = it->second; - return true; - } - else - { - return false; - } - } - - bool Matches(const OrthancPlugins::ParsedDicomFile& dicom) const - { - for (Filters::const_iterator it = filters_.begin(); - it != filters_.end(); ++it) - { - std::string value; - if (!dicom.GetTag(value, it->first, true)) + if (lower.size() == 0 && upper.size() == 0) { return false; } - if (!Matches(value, it->second)) + if (lower.size() == 0) + { + return v <= upper; + } + + if (upper.size() == 0) + { + return v >= lower; + } + + return (v >= lower && v <= upper); + } + + + static bool ApplyListConstraint(const std::string& value, + const std::string& constraint) + { + std::string v1(value); + OrthancPlugins::ToLowerCase(v1); + + std::vector<std::string> items; + OrthancPlugins::TokenizeString(items, constraint, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + std::string lower(items[i]); + OrthancPlugins::ToLowerCase(lower); + if (lower == v1) + { + return true; + } + } + + return false; + } + + + static std::string WildcardToRegularExpression(const std::string& source) + { + std::string result = source; + + // Escape all special characters + boost::replace_all(result, "\\", "\\\\"); + boost::replace_all(result, "^", "\\^"); + boost::replace_all(result, ".", "\\."); + boost::replace_all(result, "$", "\\$"); + boost::replace_all(result, "|", "\\|"); + boost::replace_all(result, "(", "\\("); + boost::replace_all(result, ")", "\\)"); + boost::replace_all(result, "[", "\\["); + boost::replace_all(result, "]", "\\]"); + boost::replace_all(result, "+", "\\+"); + boost::replace_all(result, "/", "\\/"); + boost::replace_all(result, "{", "\\{"); + boost::replace_all(result, "}", "\\}"); + + // Convert wildcards '*' and '?' to their regex equivalents + boost::replace_all(result, "?", "."); + boost::replace_all(result, "*", ".*"); + + return result; + } + + + static bool Matches(const std::string& value, + const std::string& constraint) + { + // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained + // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html + + if (constraint.find('-') != std::string::npos) + { + return ApplyRangeConstraint(value, constraint); + } + + if (constraint.find('\\') != std::string::npos) + { + return ApplyListConstraint(value, constraint); + } + + if (constraint.find('*') != std::string::npos || + constraint.find('?') != std::string::npos) + { + boost::regex pattern(WildcardToRegularExpression(constraint), + boost::regex::icase /* case insensitive search */); + return boost::regex_match(value, pattern); + } + else + { + std::string v(value), c(constraint); + OrthancPlugins::ToLowerCase(v); + OrthancPlugins::ToLowerCase(c); + return v == c; + } + } + + + + static void AddResultAttributesForLevel(std::list<gdcm::Tag>& result, + QueryLevel level) + { + switch (level) + { + case QueryLevel_Study: + // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2 + result.push_back(gdcm::Tag(0x0008, 0x0005)); // Specific Character Set + result.push_back(gdcm::Tag(0x0008, 0x0020)); // Study Date + result.push_back(gdcm::Tag(0x0008, 0x0030)); // Study Time + result.push_back(gdcm::Tag(0x0008, 0x0050)); // Accession Number + result.push_back(gdcm::Tag(0x0008, 0x0056)); // Instance Availability + result.push_back(gdcm::Tag(0x0008, 0x0061)); // Modalities in Study + result.push_back(gdcm::Tag(0x0008, 0x0090)); // Referring Physician's Name + result.push_back(gdcm::Tag(0x0008, 0x0201)); // Timezone Offset From UTC + //result.push_back(gdcm::Tag(0x0008, 0x1190)); // Retrieve URL => SPECIAL CASE + result.push_back(gdcm::Tag(0x0010, 0x0010)); // Patient's Name + result.push_back(gdcm::Tag(0x0010, 0x0020)); // Patient ID + result.push_back(gdcm::Tag(0x0010, 0x0030)); // Patient's Birth Date + result.push_back(gdcm::Tag(0x0010, 0x0040)); // Patient's Sex + result.push_back(gdcm::Tag(0x0020, 0x000D)); // Study Instance UID + result.push_back(gdcm::Tag(0x0020, 0x0010)); // Study ID + result.push_back(gdcm::Tag(0x0020, 0x1206)); // Number of Study Related Series + result.push_back(gdcm::Tag(0x0020, 0x1208)); // Number of Study Related Instances + break; + + case QueryLevel_Series: + // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2a + result.push_back(gdcm::Tag(0x0008, 0x0005)); // Specific Character Set + result.push_back(gdcm::Tag(0x0008, 0x0056)); // Modality + result.push_back(gdcm::Tag(0x0008, 0x0201)); // Timezone Offset From UTC + result.push_back(gdcm::Tag(0x0008, 0x103E)); // Series Description + //result.push_back(gdcm::Tag(0x0008, 0x1190)); // Retrieve URL => SPECIAL CASE + result.push_back(gdcm::Tag(0x0020, 0x000E)); // Series Instance UID + result.push_back(gdcm::Tag(0x0020, 0x0011)); // Series Number + result.push_back(gdcm::Tag(0x0020, 0x1209)); // Number of Series Related Instances + result.push_back(gdcm::Tag(0x0040, 0x0244)); // Performed Procedure Step Start Date + result.push_back(gdcm::Tag(0x0040, 0x0245)); // Performed Procedure Step Start Time + result.push_back(gdcm::Tag(0x0040, 0x0275)); // Request Attribute Sequence + break; + + case QueryLevel_Instance: + // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2b + result.push_back(gdcm::Tag(0x0008, 0x0005)); // Specific Character Set + result.push_back(gdcm::Tag(0x0008, 0x0016)); // SOP Class UID + result.push_back(gdcm::Tag(0x0008, 0x0018)); // SOP Instance UID + result.push_back(gdcm::Tag(0x0008, 0x0056)); // Instance Availability + result.push_back(gdcm::Tag(0x0008, 0x0201)); // Timezone Offset From UTC + result.push_back(gdcm::Tag(0x0008, 0x1190)); // Retrieve URL + result.push_back(gdcm::Tag(0x0020, 0x0013)); // Instance Number + result.push_back(gdcm::Tag(0x0028, 0x0010)); // Rows + result.push_back(gdcm::Tag(0x0028, 0x0011)); // Columns + result.push_back(gdcm::Tag(0x0028, 0x0100)); // Bits Allocated + result.push_back(gdcm::Tag(0x0028, 0x0008)); // Number of Frames + break; + + default: + throw std::runtime_error("Internal error"); + } + } + + + + public: + ModuleMatcher(const OrthancPluginHttpRequest* request) : + dictionary_(gdcm::Global::GetInstance().GetDicts().GetPublicDict()), + fuzzy_(false), + offset_(0), + limit_(0), + includeAllFields_(false) + { + for (int32_t i = 0; i < request->getCount; i++) + { + std::string key(request->getKeys[i]); + std::string value(request->getValues[i]); + + if (key == "limit") + { + limit_ = boost::lexical_cast<unsigned int>(value); + } + else if (key == "offset") + { + offset_ = boost::lexical_cast<unsigned int>(value); + } + else if (key == "fuzzymatching") + { + if (value == "true") + { + fuzzy_ = true; + } + else if (value == "false") + { + fuzzy_ = false; + } + else + { + throw std::runtime_error("Not a proper value for fuzzy matching (true or false): " + value); + } + } + else if (key == "includefield") + { + if (key == "all") + { + includeAllFields_ = true; + } + else + { + includeFields_.push_back(ParseTag(key)); + } + } + else + { + filters_[ParseTag(key)] = value; + } + } + } + + unsigned int GetLimit() const + { + return limit_; + } + + unsigned int GetOffset() const + { + return offset_; + } + + void AddFilter(const gdcm::Tag& tag, + const std::string& constraint) + { + filters_[tag] = constraint; + } + + bool LookupExactFilter(std::string& constraint, + const gdcm::Tag& tag) const + { + Filters::const_iterator it = filters_.find(tag); + if (it != filters_.end() && + !IsWildcard(it->second)) + { + constraint = it->second; + return true; + } + else { return false; } } - return true; - } - - - void ExtractFields(gdcm::DataSet& result, - const OrthancPlugins::ParsedDicomFile& dicom, - const std::string& wadoBase, - QueryLevel level) const - { - std::list<gdcm::Tag> fields = includeFields_; - - // The list of attributes for this query level - AddResultAttributesForLevel(fields, level); + bool Matches(const OrthancPlugins::ParsedDicomFile& dicom) const + { + for (Filters::const_iterator it = filters_.begin(); + it != filters_.end(); ++it) + { + std::string value; + if (!dicom.GetTag(value, it->first, true)) + { + return false; + } - // All other attributes passed as query keys - for (Filters::const_iterator it = filters_.begin(); - it != filters_.end(); ++it) - { - fields.push_back(it->first); - } + if (!Matches(value, it->second)) + { + return false; + } + } - // For instances and series, add all Study-level attributes if - // {StudyInstanceUID} is not specified. - if ((level == QueryLevel_Instance || level == QueryLevel_Series) - && filters_.find(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID) == filters_.end() - ) - { - AddResultAttributesForLevel(fields, QueryLevel_Study); + return true; } - // For instances, add all Series-level attributes if - // {SeriesInstanceUID} is not specified. - if (level == QueryLevel_Instance - && filters_.find(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID) == filters_.end() - ) + + void ExtractFields(gdcm::DataSet& result, + const OrthancPlugins::ParsedDicomFile& dicom, + const std::string& wadoBase, + QueryLevel level) const { - AddResultAttributesForLevel(fields, QueryLevel_Series); - } + std::list<gdcm::Tag> fields = includeFields_; + + // The list of attributes for this query level + AddResultAttributesForLevel(fields, level); - // Copy all the required fields to the target - for (std::list<gdcm::Tag>::const_iterator - it = fields.begin(); it != fields.end(); it++) - { - if (dicom.GetDataSet().FindDataElement(*it)) + // All other attributes passed as query keys + for (Filters::const_iterator it = filters_.begin(); + it != filters_.end(); ++it) { - const gdcm::DataElement& element = dicom.GetDataSet().GetDataElement(*it); - result.Replace(element); + fields.push_back(it->first); } - } + + // For instances and series, add all Study-level attributes if + // {StudyInstanceUID} is not specified. + if ((level == QueryLevel_Instance || level == QueryLevel_Series) + && filters_.find(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID) == filters_.end() + ) + { + AddResultAttributesForLevel(fields, QueryLevel_Study); + } - // Set the retrieve URL for WADO-RS - std::string url = (wadoBase + "/studies/" + - dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, "", true)); + // For instances, add all Series-level attributes if + // {SeriesInstanceUID} is not specified. + if (level == QueryLevel_Instance + && filters_.find(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID) == filters_.end() + ) + { + AddResultAttributesForLevel(fields, QueryLevel_Series); + } - if (level == QueryLevel_Series || level == QueryLevel_Instance) - { - url += "/series/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, "", true); - } + // Copy all the required fields to the target + for (std::list<gdcm::Tag>::const_iterator + it = fields.begin(); it != fields.end(); it++) + { + if (dicom.GetDataSet().FindDataElement(*it)) + { + const gdcm::DataElement& element = dicom.GetDataSet().GetDataElement(*it); + result.Replace(element); + } + } - if (level == QueryLevel_Instance) - { - url += "/instances/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, "", true); - } + // Set the retrieve URL for WADO-RS + std::string url = (wadoBase + "/studies/" + + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, "", true)); + + if (level == QueryLevel_Series || level == QueryLevel_Instance) + { + url += "/series/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, "", true); + } + + if (level == QueryLevel_Instance) + { + url += "/instances/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, "", true); + } - gdcm::DataElement element(OrthancPlugins::DICOM_TAG_RETRIEVE_URL); - element.SetByteValue(url.c_str(), url.size()); - result.Replace(element); - } -}; + gdcm::DataElement element(OrthancPlugins::DICOM_TAG_RETRIEVE_URL); + element.SetByteValue(url.c_str(), url.size()); + result.Replace(element); + } + }; -class CandidateResources -{ -private: - typedef std::set<std::string> Resources; - - bool all_; - QueryLevel level_; - Resources resources_; - - static bool CallLookup(std::string& orthancId, - const std::string& dicomId, - char* (lookup) (OrthancPluginContext*, const char*)) + class CandidateResources { - bool result = false; - - char* tmp = lookup(context_, dicomId.c_str()); - if (tmp != NULL) - { - orthancId = tmp; - result = true; - } - - OrthancPluginFreeString(context_, tmp); - - return result; - } - + private: + typedef std::set<std::string> Resources; - void FilterByIdentifierInternal(const ModuleMatcher& matcher, - const gdcm::Tag& tag, - char* (lookup) (OrthancPluginContext*, const char*)) - { - std::string orthancId, dicomId; - - if (!matcher.LookupExactFilter(dicomId, tag)) - { - // There is no restriction at this level - return; - } - - if (CallLookup(orthancId, dicomId, lookup) && - (all_ || resources_.find(orthancId) != resources_.end())) - { - // There remains a single candidate resource - resources_.clear(); - resources_.insert(orthancId); - } - else - { - // No matching resource remains - resources_.clear(); - } - - all_ = false; - } - + bool all_; + QueryLevel level_; + Resources resources_; - bool PickOneInstance(std::string& instance, - const std::string& resource) const - { - if (level_ == QueryLevel_Instance) - { - instance = resource; - return true; - } - - std::string uri; - if (level_ == QueryLevel_Study) - { - uri = "/studies/" + resource + "/instances"; - } - else - { - assert(level_ == QueryLevel_Series); - uri = "/series/" + resource + "/instances"; - } - - Json::Value instances; - if (!OrthancPlugins::RestApiGetJson(instances, context_, uri) || - instances.type() != Json::arrayValue || - instances.size() == 0) - { - return false; - } - else + static bool CallLookup(std::string& orthancId, + const std::string& dicomId, + char* (lookup) (OrthancPluginContext*, const char*)) { - instance = instances[0]["ID"].asString(); - return true; - } - } - - -public: - CandidateResources() : all_(true), level_(QueryLevel_Study) - { - } + bool result = false; - void GoDown() - { - std::string baseUri; - std::string nextLevel; - switch (level_) - { - case QueryLevel_Study: - baseUri = "/studies/"; - nextLevel = "Series"; - break; + char* tmp = lookup(context_, dicomId.c_str()); + if (tmp != NULL) + { + orthancId = tmp; + result = true; + } - case QueryLevel_Series: - baseUri = "/series/"; - nextLevel = "Instances"; - break; + OrthancPluginFreeString(context_, tmp); - default: - throw std::runtime_error("Internal error"); + return result; } - if (!all_) + void FilterByIdentifierInternal(const ModuleMatcher& matcher, + const gdcm::Tag& tag, + char* (lookup) (OrthancPluginContext*, const char*)) { - Resources children; - - for (Resources::const_iterator it = resources_.begin(); - it != resources_.end(); it++) + std::string orthancId, dicomId; + + if (!matcher.LookupExactFilter(dicomId, tag)) { - Json::Value tmp; - if (OrthancPlugins::RestApiGetJson(tmp, context_, baseUri + *it) && - tmp.type() == Json::objectValue && - tmp.isMember(nextLevel) && - tmp[nextLevel].type() == Json::arrayValue) - { - for (Json::Value::ArrayIndex i = 0; i < tmp[nextLevel].size(); i++) - { - children.insert(tmp[nextLevel][i].asString()); - } - } + // There is no restriction at this level + return; } - resources_ = children; + if (CallLookup(orthancId, dicomId, lookup) && + (all_ || resources_.find(orthancId) != resources_.end())) + { + // There remains a single candidate resource + resources_.clear(); + resources_.insert(orthancId); + } + else + { + // No matching resource remains + resources_.clear(); + } + + all_ = false; } - switch (level_) + bool PickOneInstance(std::string& instance, + const std::string& resource) const { - case QueryLevel_Study: - level_ = QueryLevel_Series; - break; + if (level_ == QueryLevel_Instance) + { + instance = resource; + return true; + } - case QueryLevel_Series: - level_ = QueryLevel_Instance; - break; + std::string uri; + if (level_ == QueryLevel_Study) + { + uri = "/studies/" + resource + "/instances"; + } + else + { + assert(level_ == QueryLevel_Series); + uri = "/series/" + resource + "/instances"; + } - default: - throw std::runtime_error("Internal error"); + Json::Value instances; + if (!OrthancPlugins::RestApiGetJson(instances, context_, uri) || + instances.type() != Json::arrayValue || + instances.size() == 0) + { + return false; + } + else + { + instance = instances[0]["ID"].asString(); + return true; + } } - } - void FilterByIdentifier(const ModuleMatcher& matcher) - { - switch (level_) + public: + CandidateResources() : all_(true), level_(QueryLevel_Study) { - case QueryLevel_Study: - FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, - OrthancPluginLookupStudy); - FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_ACCESSION_NUMBER, - OrthancPluginLookupStudyWithAccessionNumber); - break; - - case QueryLevel_Series: - FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, - OrthancPluginLookupSeries); - break; + } - case QueryLevel_Instance: - FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, - OrthancPluginLookupInstance); - break; - - default: - throw std::runtime_error("Internal error"); - } - } - - - void Flatten(std::list<std::string>& result) const - { - std::string instance; - - result.clear(); - - if (all_) + void GoDown() { - std::string uri; + std::string baseUri; + std::string nextLevel; switch (level_) { case QueryLevel_Study: - uri = "/studies/"; + baseUri = "/studies/"; + nextLevel = "Series"; break; case QueryLevel_Series: - uri = "/series/"; - break; - - case QueryLevel_Instance: - uri = "/instances/"; + baseUri = "/series/"; + nextLevel = "Instances"; break; default: throw std::runtime_error("Internal error"); } - Json::Value tmp; - if (OrthancPlugins::RestApiGetJson(tmp, context_, uri) && - tmp.type() == Json::arrayValue) + + if (!all_) + { + Resources children; + + for (Resources::const_iterator it = resources_.begin(); + it != resources_.end(); it++) + { + Json::Value tmp; + if (OrthancPlugins::RestApiGetJson(tmp, context_, baseUri + *it) && + tmp.type() == Json::objectValue && + tmp.isMember(nextLevel) && + tmp[nextLevel].type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < tmp[nextLevel].size(); i++) + { + children.insert(tmp[nextLevel][i].asString()); + } + } + } + + resources_ = children; + } + + + switch (level_) + { + case QueryLevel_Study: + level_ = QueryLevel_Series; + break; + + case QueryLevel_Series: + level_ = QueryLevel_Instance; + break; + + default: + throw std::runtime_error("Internal error"); + } + } + + + void FilterByIdentifier(const ModuleMatcher& matcher) + { + switch (level_) { - for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++) + case QueryLevel_Study: + FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, + OrthancPluginLookupStudy); + FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_ACCESSION_NUMBER, + OrthancPluginLookupStudyWithAccessionNumber); + break; + + case QueryLevel_Series: + FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, + OrthancPluginLookupSeries); + break; + + case QueryLevel_Instance: + FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, + OrthancPluginLookupInstance); + break; + + default: + throw std::runtime_error("Internal error"); + } + } + + + void Flatten(std::list<std::string>& result) const + { + std::string instance; + + result.clear(); + + if (all_) + { + std::string uri; + switch (level_) { - if (PickOneInstance(instance, tmp[i].asString())) + case QueryLevel_Study: + uri = "/studies/"; + break; + + case QueryLevel_Series: + uri = "/series/"; + break; + + case QueryLevel_Instance: + uri = "/instances/"; + break; + + default: + throw std::runtime_error("Internal error"); + } + + Json::Value tmp; + if (OrthancPlugins::RestApiGetJson(tmp, context_, uri) && + tmp.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++) + { + if (PickOneInstance(instance, tmp[i].asString())) + { + result.push_back(instance); + } + } + } + } + else + { + for (Resources::const_iterator + it = resources_.begin(); it != resources_.end(); it++) + { + if (PickOneInstance(instance, *it)) { result.push_back(instance); } } } } - else - { - for (Resources::const_iterator - it = resources_.begin(); it != resources_.end(); it++) - { - if (PickOneInstance(instance, *it)) - { - result.push_back(instance); - } - } - } - } -}; - - - -class SearchResults -{ -private: - typedef std::list<gdcm::DataSet*> Results; - - Results results_; - -public: - ~SearchResults() - { - for (Results::iterator it = results_.begin(); - it != results_.end(); it++) - { - delete *it; - } - } - - void Add(const OrthancPlugins::ParsedDicomFile& dicom, - const ModuleMatcher& matcher, - const std::string& wadoBase, - QueryLevel level) - { - std::auto_ptr<gdcm::DataSet> result(new gdcm::DataSet); - matcher.ExtractFields(*result, dicom, wadoBase, level); - results_.push_back(result.release()); - } + }; +} - void Answer(OrthancPluginContext* context, - OrthancPluginRestOutput* output, - bool isXml) - { - if (isXml) - { - OrthancPlugins::MultipartWriter writer("application/dicom+xml"); - - for (Results::const_iterator it = results_.begin(); - it != results_.end(); it++) - { - std::string answer; - OrthancPlugins::GenerateSingleDicomAnswer(answer, *dictionary_, **it, true); - writer.AddPart(answer); - } - - writer.Answer(context_, output); - } - else - { - OrthancPlugins::ChunkedBuffer chunks; - chunks.AddChunk("[\n"); - - std::string s = "[\n"; - - bool isFirst = true; - for (Results::const_iterator it = results_.begin(); - it != results_.end(); it++) - { - if (isFirst) - { - isFirst = false; - } - else - { - chunks.AddChunk(",\n"); - } - - std::string item; - OrthancPlugins::GenerateSingleDicomAnswer(item, *dictionary_, **it, false); - chunks.AddChunk(item); - } - - chunks.AddChunk("]\n"); - - std::string answer; - chunks.Flatten(answer); - OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json"); - } - } -}; @@ -815,7 +740,9 @@ candidates.Flatten(resources); std::string wadoBase = OrthancPlugins::Configuration::GetBaseUrl(configuration_, request) + "/wado-rs"; - SearchResults results; + + OrthancPlugins::DicomResults results(*dictionary_, IsXmlExpected(request), true); + for (std::list<std::string>::const_iterator it = resources.begin(); it != resources.end(); it++) { @@ -825,12 +752,14 @@ OrthancPlugins::ParsedDicomFile dicom(file); if (matcher.Matches(dicom)) { - results.Add(dicom, matcher, wadoBase, level); + std::auto_ptr<gdcm::DataSet> result(new gdcm::DataSet); + matcher.ExtractFields(*result, dicom, wadoBase, level); + results.Add(*result); } } } - results.Answer(context_, output, IsXmlExpected(request)); + results.Answer(context_, output); }
--- a/Plugin/StowRs.cpp Thu Apr 30 15:53:07 2015 +0200 +++ b/Plugin/StowRs.cpp Thu May 07 17:07:25 2015 +0200 @@ -223,7 +223,7 @@ SetSequenceTag(result, OrthancPlugins::DICOM_TAG_FAILED_SOP_SEQUENCE, failed); SetSequenceTag(result, OrthancPlugins::DICOM_TAG_REFERENCED_SOP_SEQUENCE, success); - OrthancPlugins::AnswerDicom(context_, output, *dictionary_, result, isXml); + OrthancPlugins::AnswerDicom(context_, output, *dictionary_, result, isXml, false); return 0; }
--- a/Plugin/WadoRs.cpp Thu Apr 30 15:53:07 2015 +0200 +++ b/Plugin/WadoRs.cpp Thu May 07 17:07:25 2015 +0200 @@ -22,6 +22,7 @@ #include "../Core/Configuration.h" #include "../Core/Dicom.h" +#include "../Core/DicomResults.h" #include "../Core/MultipartWriter.h" @@ -52,7 +53,62 @@ OrthancPlugins::ToLowerCase(s); if (s != "application/dicom") { - std::string s = "This WADO-RS plugin only supports application/dicom return type (" + accept + ")"; + std::string s = "This WADO-RS plugin only supports application/dicom return type for DICOM retrieval (" + accept + ")"; + OrthancPluginLogError(context_, s.c_str()); + return false; + } + } + + if (attributes.find("transfer-syntax") != attributes.end()) + { + std::string s = "This WADO-RS plugin cannot change the transfer syntax to " + attributes["transfer-syntax"]; + OrthancPluginLogError(context_, s.c_str()); + return false; + } + + return true; +} + + + +static bool AcceptMetadata(const OrthancPluginHttpRequest* request, + bool& isXml) +{ + isXml = true; + + std::string accept; + + if (!OrthancPlugins::LookupHttpHeader(accept, request, "accept")) + { + // By default, return "multipart/related; type=application/dicom+xml;" + return true; + } + + std::string application; + std::map<std::string, std::string> attributes; + OrthancPlugins::ParseContentType(application, attributes, accept); + + if (application == "application/json") + { + isXml = false; + return true; + } + + if (application != "multipart/related" && + application != "*/*") + { + std::string s = "This WADO-RS plugin cannot generate the following content type: " + accept; + OrthancPluginLogError(context_, s.c_str()); + return false; + } + + if (attributes.find("type") != attributes.end()) + { + std::string s = attributes["type"]; + OrthancPlugins::ToLowerCase(s); + if (s != "application/dicom+xml") + { + std::string s = "This WADO-RS plugin only supports application/json or application/dicom+xml return types for metadata (" + accept + ")"; OrthancPluginLogError(context_, s.c_str()); return false; } @@ -99,41 +155,195 @@ +static void AnswerMetadata(OrthancPluginRestOutput* output, + const std::string& resource, + bool isInstance, + bool isXml) +{ + std::list<std::string> files; + if (isInstance) + { + files.push_back(resource + "/file"); + } + else + { + Json::Value instances; + if (!OrthancPlugins::RestApiGetJson(instances, context_, resource + "/instances")) + { + // Internal error + OrthancPluginSendHttpStatusCode(context_, output, 400); + return; + } + + for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++) + { + files.push_back("/instances/" + instances[i]["ID"].asString() + "/file"); + } + } + + OrthancPlugins::DicomResults results(*dictionary_, isXml, true); + + for (std::list<std::string>::const_iterator + it = files.begin(); it != files.end(); ++it) + { + std::string content; + if (OrthancPlugins::RestApiGetString(content, context_, *it)) + { + OrthancPlugins::ParsedDicomFile dicom(content); + results.Add(dicom.GetDataSet()); + } + } + + results.Answer(context_, output); +} + + + + +static bool LocateStudy(OrthancPluginRestOutput* output, + std::string& uri, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context_, output, "GET"); + return false; + } + + std::string id; + + { + char* tmp = OrthancPluginLookupStudy(context_, request->groups[0]); + if (tmp == NULL) + { + std::string s = "Accessing an inexistent study with WADO-RS: " + std::string(request->groups[0]); + OrthancPluginLogError(context_, s.c_str()); + OrthancPluginSendHttpStatusCode(context_, output, 404); + return false; + } + + id.assign(tmp); + OrthancPluginFreeString(context_, tmp); + } + + uri = "/studies/" + id; + return true; +} + + +static bool LocateSeries(OrthancPluginRestOutput* output, + std::string& uri, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context_, output, "GET"); + return false; + } + + std::string id; + + { + char* tmp = OrthancPluginLookupSeries(context_, request->groups[1]); + if (tmp == NULL) + { + std::string s = "Accessing an inexistent series with WADO-RS: " + std::string(request->groups[1]); + OrthancPluginLogError(context_, s.c_str()); + OrthancPluginSendHttpStatusCode(context_, output, 404); + return false; + } + + id.assign(tmp); + OrthancPluginFreeString(context_, tmp); + } + + Json::Value study; + if (!OrthancPlugins::RestApiGetJson(study, context_, "/series/" + id + "/study")) + { + OrthancPluginSendHttpStatusCode(context_, output, 404); + return false; + } + + if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0])) + { + std::string s = "No series " + std::string(request->groups[1]) + " in study " + std::string(request->groups[0]); + OrthancPluginLogError(context_, s.c_str()); + OrthancPluginSendHttpStatusCode(context_, output, 404); + return false; + } + + uri = "/series/" + id; + return true; +} + + +static bool LocateInstance(OrthancPluginRestOutput* output, + std::string& uri, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context_, output, "GET"); + return false; + } + + std::string id; + + { + char* tmp = OrthancPluginLookupInstance(context_, request->groups[2]); + if (tmp == NULL) + { + std::string s = "Accessing an inexistent instance with WADO-RS: " + std::string(request->groups[2]); + OrthancPluginLogError(context_, s.c_str()); + OrthancPluginSendHttpStatusCode(context_, output, 404); + return false; + } + + id.assign(tmp); + OrthancPluginFreeString(context_, tmp); + } + + Json::Value study, series; + if (!OrthancPlugins::RestApiGetJson(series, context_, "/instances/" + id + "/series") || + !OrthancPlugins::RestApiGetJson(study, context_, "/instances/" + id + "/study")) + { + OrthancPluginSendHttpStatusCode(context_, output, 404); + return false; + } + + if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0]) || + series["MainDicomTags"]["SeriesInstanceUID"].asString() != std::string(request->groups[1])) + { + std::string s = ("No instance " + std::string(request->groups[2]) + + " in study " + std::string(request->groups[0]) + " or " + + " in series " + std::string(request->groups[1])); + OrthancPluginLogError(context_, s.c_str()); + OrthancPluginSendHttpStatusCode(context_, output, 404); + return false; + } + + uri = "/instances/" + id; + return true; +} + + int32_t RetrieveDicomStudy(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request) { try { - if (request->method != OrthancPluginHttpMethod_Get) - { - OrthancPluginSendMethodNotAllowed(context_, output, "GET"); - return 0; - } - if (!AcceptMultipartDicom(request)) { OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */); - return 0; + return false; } - std::string id; - + std::string uri; + if (LocateStudy(output, uri, request)) { - char* tmp = OrthancPluginLookupStudy(context_, request->groups[0]); - if (tmp == NULL) - { - std::string s = "Accessing an inexistent study with WADO-RS: " + std::string(request->groups[0]); - OrthancPluginLogError(context_, s.c_str()); - OrthancPluginSendHttpStatusCode(context_, output, 404); - return 0; - } - - id.assign(tmp); - OrthancPluginFreeString(context_, tmp); + AnswerListOfDicomInstances(output, uri); } - - AnswerListOfDicomInstances(output, "/studies/" + id); return 0; } @@ -151,50 +361,17 @@ { try { - if (request->method != OrthancPluginHttpMethod_Get) - { - OrthancPluginSendMethodNotAllowed(context_, output, "GET"); - return 0; - } - if (!AcceptMultipartDicom(request)) { OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */); - return 0; + return false; } - std::string id; - + std::string uri; + if (LocateSeries(output, uri, request)) { - char* tmp = OrthancPluginLookupSeries(context_, request->groups[1]); - if (tmp == NULL) - { - std::string s = "Accessing an inexistent series with WADO-RS: " + std::string(request->groups[1]); - OrthancPluginLogError(context_, s.c_str()); - OrthancPluginSendHttpStatusCode(context_, output, 404); - return 0; - } - - id.assign(tmp); - OrthancPluginFreeString(context_, tmp); + AnswerListOfDicomInstances(output, uri); } - - Json::Value study; - if (!OrthancPlugins::RestApiGetJson(study, context_, "/series/" + id + "/study")) - { - OrthancPluginSendHttpStatusCode(context_, output, 404); - return 0; - } - - if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0])) - { - std::string s = "No series " + std::string(request->groups[1]) + " in study " + std::string(request->groups[0]); - OrthancPluginLogError(context_, s.c_str()); - OrthancPluginSendHttpStatusCode(context_, output, 404); - return 0; - } - - AnswerListOfDicomInstances(output, "/series/" + id); return 0; } @@ -213,62 +390,55 @@ { try { - if (request->method != OrthancPluginHttpMethod_Get) - { - OrthancPluginSendMethodNotAllowed(context_, output, "GET"); - return 0; - } - if (!AcceptMultipartDicom(request)) { OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */); - return 0; + return false; } - std::string id; - + std::string uri; + if (LocateInstance(output, uri, request)) { - char* tmp = OrthancPluginLookupInstance(context_, request->groups[2]); - if (tmp == NULL) + OrthancPlugins::MultipartWriter writer("application/dicom"); + std::string dicom; + if (OrthancPlugins::RestApiGetString(dicom, context_, uri + "/file")) { - std::string s = "Accessing an inexistent instance with WADO-RS: " + std::string(request->groups[2]); - OrthancPluginLogError(context_, s.c_str()); - OrthancPluginSendHttpStatusCode(context_, output, 404); - return 0; + writer.AddPart(dicom); } - id.assign(tmp); - OrthancPluginFreeString(context_, tmp); - } - - Json::Value study, series; - if (!OrthancPlugins::RestApiGetJson(series, context_, "/instances/" + id + "/series") || - !OrthancPlugins::RestApiGetJson(study, context_, "/instances/" + id + "/study")) - { - OrthancPluginSendHttpStatusCode(context_, output, 404); - return 0; + writer.Answer(context_, output); } - if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0]) || - series["MainDicomTags"]["SeriesInstanceUID"].asString() != std::string(request->groups[1])) + return 0; + } + catch (std::runtime_error& e) + { + OrthancPluginLogError(context_, e.what()); + return -1; + } +} + + + +int32_t RetrieveStudyMetadata(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + try + { + bool isXml; + if (!AcceptMetadata(request, isXml)) { - std::string s = ("No instance " + std::string(request->groups[2]) + - " in study " + std::string(request->groups[0]) + " or " + - " in series " + std::string(request->groups[1])); - OrthancPluginLogError(context_, s.c_str()); - OrthancPluginSendHttpStatusCode(context_, output, 404); - return 0; + OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */); + return false; } - OrthancPlugins::MultipartWriter writer("application/dicom"); - std::string dicom; - if (OrthancPlugins::RestApiGetString(dicom, context_, "/instances/" + id + "/file")) + std::string uri; + if (LocateStudy(output, uri, request)) { - writer.AddPart(dicom); + AnswerMetadata(output, uri, false, isXml); } - writer.Answer(context_, output); - return 0; } catch (std::runtime_error& e) @@ -277,3 +447,61 @@ return -1; } } + + +int32_t RetrieveSeriesMetadata(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + try + { + bool isXml; + if (!AcceptMetadata(request, isXml)) + { + OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */); + return false; + } + + std::string uri; + if (LocateSeries(output, uri, request)) + { + AnswerMetadata(output, uri, false, isXml); + } + + return 0; + } + catch (std::runtime_error& e) + { + OrthancPluginLogError(context_, e.what()); + return -1; + } +} + + +int32_t RetrieveInstanceMetadata(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + try + { + bool isXml; + if (!AcceptMetadata(request, isXml)) + { + OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */); + return false; + } + + std::string uri; + if (LocateInstance(output, uri, request)) + { + AnswerMetadata(output, uri, true, isXml); + } + + return 0; + } + catch (std::runtime_error& e) + { + OrthancPluginLogError(context_, e.what()); + return -1; + } +}
--- a/Plugin/WadoRs.h Thu Apr 30 15:53:07 2015 +0200 +++ b/Plugin/WadoRs.h Thu May 07 17:07:25 2015 +0200 @@ -34,3 +34,15 @@ int32_t RetrieveDicomInstance(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request); + +int32_t RetrieveStudyMetadata(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request); + +int32_t RetrieveSeriesMetadata(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request); + +int32_t RetrieveInstanceMetadata(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request);
--- a/Samples/JavaScript/qido-rs.js Thu Apr 30 15:53:07 2015 +0200 +++ b/Samples/JavaScript/qido-rs.js Thu May 07 17:07:25 2015 +0200 @@ -57,7 +57,7 @@ $('#qido-series-results').append( '<li>' + patientId + ' - ' + patientName + ' - ' + studyDescription + ' - ' + seriesDescription + - + ' - ' + url + '</li>'); + ' - ' + '<a href="' + url + '">WADO-RS URL</a></li>'); } }, error: function() {
--- a/Status.txt Thu Apr 30 15:53:07 2015 +0200 +++ b/Status.txt Thu May 07 17:07:25 2015 +0200 @@ -27,10 +27,16 @@ ================================ 6.5.4 WADO-RS / RetrieveFrames 6.5.5 WADO-RS / RetrieveBulkdata +================================ + +Not supported. + + +================================ 6.5.6 WADO-RS / RetrieveMetadata ================================ -Not supported. +Supported.