Mercurial > hg > orthanc
diff OrthancServer/Sources/OrthancFindRequestHandler.cpp @ 4092:fb64d481940a
making the "framework" branch the new "default"
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 30 Jun 2020 15:53:17 +0200 |
parents | 9214e3a7b0a2 |
children | c02a2d9efbc2 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Tue Jun 30 15:53:17 2020 +0200 @@ -0,0 +1,733 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "PrecompiledHeadersServer.h" +#include "OrthancFindRequestHandler.h" + +#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../OrthancFramework/Sources/Logging.h" +#include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h" +#include "../../OrthancFramework/Sources/MetricsRegistry.h" +#include "OrthancConfiguration.h" +#include "Search/DatabaseLookup.h" +#include "ServerContext.h" +#include "ServerToolbox.h" + +#include <boost/regex.hpp> + + +namespace Orthanc +{ + static void GetChildren(std::list<std::string>& target, + ServerIndex& index, + const std::list<std::string>& source) + { + target.clear(); + + for (std::list<std::string>::const_iterator + it = source.begin(); it != source.end(); ++it) + { + std::list<std::string> tmp; + index.GetChildren(tmp, *it); + target.splice(target.end(), tmp); + } + } + + + static void StoreSetOfStrings(DicomMap& result, + const DicomTag& tag, + const std::set<std::string>& values) + { + bool isFirst = true; + + std::string s; + for (std::set<std::string>::const_iterator + it = values.begin(); it != values.end(); ++it) + { + if (isFirst) + { + isFirst = false; + } + else + { + s += "\\"; + } + + s += *it; + } + + result.SetValue(tag, s, false); + } + + + static void ComputePatientCounters(DicomMap& result, + ServerIndex& index, + const std::string& patient, + const DicomMap& query) + { + std::list<std::string> studies; + index.GetChildren(studies, patient); + + if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES)) + { + result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, + boost::lexical_cast<std::string>(studies.size()), false); + } + + if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) && + !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) + { + return; + } + + std::list<std::string> series; + GetChildren(series, index, studies); + studies.clear(); // This information is useless below + + if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES)) + { + result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, + boost::lexical_cast<std::string>(series.size()), false); + } + + if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) + { + return; + } + + std::list<std::string> instances; + GetChildren(instances, index, series); + + if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) + { + result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, + boost::lexical_cast<std::string>(instances.size()), false); + } + } + + + static void ComputeStudyCounters(DicomMap& result, + ServerContext& context, + const std::string& study, + const DicomMap& query) + { + ServerIndex& index = context.GetIndex(); + + std::list<std::string> series; + index.GetChildren(series, study); + + if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES)) + { + result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, + boost::lexical_cast<std::string>(series.size()), false); + } + + if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) + { + std::set<std::string> values; + + for (std::list<std::string>::const_iterator + it = series.begin(); it != series.end(); ++it) + { + DicomMap tags; + if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series)) + { + const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); + + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + values.insert(value->GetContent()); + } + } + } + + StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values); + } + + if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) && + !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) + { + return; + } + + std::list<std::string> instances; + GetChildren(instances, index, series); + + if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES)) + { + result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, + boost::lexical_cast<std::string>(instances.size()), false); + } + + if (query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) + { + std::set<std::string> values; + + for (std::list<std::string>::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + std::string value; + if (context.LookupOrReconstructMetadata(value, *it, MetadataType_Instance_SopClassUid)) + { + values.insert(value); + } + } + + StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values); + } + } + + + static void ComputeSeriesCounters(DicomMap& result, + ServerIndex& index, + const std::string& series, + const DicomMap& query) + { + std::list<std::string> instances; + index.GetChildren(instances, series); + + if (query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES)) + { + result.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, + boost::lexical_cast<std::string>(instances.size()), false); + } + } + + + static DicomMap* ComputeCounters(ServerContext& context, + const std::string& instanceId, + ResourceType level, + const DicomMap& query) + { + switch (level) + { + case ResourceType_Patient: + if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) && + !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) && + !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) + { + return NULL; + } + + break; + + case ResourceType_Study: + if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) && + !query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) && + !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) && + !query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) + { + return NULL; + } + + break; + + case ResourceType_Series: + if (!query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES)) + { + return NULL; + } + + break; + + default: + return NULL; + } + + std::string parent; + if (!context.GetIndex().LookupParent(parent, instanceId, level)) + { + throw OrthancException(ErrorCode_UnknownResource); // The resource was deleted in between + } + + std::unique_ptr<DicomMap> result(new DicomMap); + + switch (level) + { + case ResourceType_Patient: + ComputePatientCounters(*result, context.GetIndex(), parent, query); + break; + + case ResourceType_Study: + ComputeStudyCounters(*result, context, parent, query); + break; + + case ResourceType_Series: + ComputeSeriesCounters(*result, context.GetIndex(), parent, query); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + return result.release(); + } + + + static void AddAnswer(DicomFindAnswers& answers, + const DicomMap& mainDicomTags, + const Json::Value* dicomAsJson, + const DicomArray& query, + const std::list<DicomTag>& sequencesToReturn, + const DicomMap* counters, + const std::string& defaultPrivateCreator, + const std::map<uint16_t, std::string>& privateCreators) + { + DicomMap match; + + if (dicomAsJson != NULL) + { + match.FromDicomAsJson(*dicomAsJson); + } + else + { + match.Assign(mainDicomTags); + } + + DicomMap result; + + for (size_t i = 0; i < query.GetSize(); i++) + { + if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL) + { + // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052)) + result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue()); + } + else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + // Do not include the encoding, this is handled by class ParsedDicomFile + } + else + { + const DicomTag& tag = query.GetElement(i).GetTag(); + const DicomValue* value = match.TestAndGetValue(tag); + + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + result.SetValue(tag, value->GetContent(), false); + } + else + { + result.SetValue(tag, "", false); + } + } + } + + if (counters != NULL) + { + DicomArray tmp(*counters); + for (size_t i = 0; i < tmp.GetSize(); i++) + { + result.SetValue(tmp.GetElement(i).GetTag(), tmp.GetElement(i).GetValue().GetContent(), false); + } + } + + if (result.GetSize() == 0 && + sequencesToReturn.empty()) + { + LOG(WARNING) << "The C-FIND request does not return any DICOM tag"; + } + else if (sequencesToReturn.empty()) + { + answers.Add(result); + } + else if (dicomAsJson == NULL) + { + LOG(WARNING) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled"; + answers.Add(result); + } + else + { + ParsedDicomFile dicom(result, GetDefaultDicomEncoding(), + true /* be permissive, cf. issue #136 */, defaultPrivateCreator, privateCreators); + + for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin(); + tag != sequencesToReturn.end(); ++tag) + { + assert(dicomAsJson != NULL); + const Json::Value& source = (*dicomAsJson) [tag->Format()]; + + if (source.type() == Json::objectValue && + source.isMember("Type") && + source.isMember("Value") && + source["Type"].asString() == "Sequence" && + source["Value"].type() == Json::arrayValue) + { + Json::Value content = Json::arrayValue; + + for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++) + { + Json::Value item; + Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short); + content.append(item); + } + + if (tag->IsPrivate()) + { + std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag->GetGroup()); + + if (found != privateCreators.end()) + { + dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str()); + } + else + { + dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator); + } + } + else + { + dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */); + } + } + } + + answers.Add(dicom); + } + } + + + + bool OrthancFindRequestHandler::FilterQueryTag(std::string& value /* can be modified */, + ResourceType level, + const DicomTag& tag, + ModalityManufacturer manufacturer) + { + // Whatever the manufacturer, remove the GenericGroupLength tags + // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.2.html + // https://bitbucket.org/sjodogne/orthanc/issues/31/ + if (tag.GetElement() == 0x0000) + { + return false; + } + + switch (manufacturer) + { + case ModalityManufacturer_Vitrea: + // Following Denis Nesterov's mail on 2015-11-30 + if (tag == DicomTag(0x5653, 0x0010)) // "PrivateCreator = Vital Images SW 3.4" + { + return false; + } + + break; + + default: + break; + } + + return true; + } + + + bool OrthancFindRequestHandler::ApplyLuaFilter(DicomMap& target, + const DicomMap& source, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, + ModalityManufacturer manufacturer) + { + static const char* LUA_CALLBACK = "IncomingFindRequestFilter"; + + LuaScripting::Lock lock(context_.GetLuaScripting()); + + if (!lock.GetLua().IsExistingFunction(LUA_CALLBACK)) + { + return false; + } + else + { + Json::Value origin; + FormatOrigin(origin, remoteIp, remoteAet, calledAet, manufacturer); + + LuaFunctionCall call(lock.GetLua(), LUA_CALLBACK); + call.PushDicom(source); + call.PushJson(origin); + FromDcmtkBridge::ExecuteToDicom(target, call); + + return true; + } + } + + + OrthancFindRequestHandler::OrthancFindRequestHandler(ServerContext& context) : + context_(context), + maxResults_(0), + maxInstances_(0) + { + } + + + class OrthancFindRequestHandler::LookupVisitor : public ServerContext::ILookupVisitor + { + private: + DicomFindAnswers& answers_; + ServerContext& context_; + ResourceType level_; + const DicomMap& query_; + DicomArray queryAsArray_; + const std::list<DicomTag>& sequencesToReturn_; + std::string defaultPrivateCreator_; // the private creator to use if the group is not defined in the query itself + const std::map<uint16_t, std::string>& privateCreators_; // the private creators defined in the query itself + + public: + LookupVisitor(DicomFindAnswers& answers, + ServerContext& context, + ResourceType level, + const DicomMap& query, + const std::list<DicomTag>& sequencesToReturn, + const std::map<uint16_t, std::string>& privateCreators) : + answers_(answers), + context_(context), + level_(level), + query_(query), + queryAsArray_(query), + sequencesToReturn_(sequencesToReturn), + privateCreators_(privateCreators) + { + answers_.SetComplete(false); + + { + OrthancConfiguration::ReaderLock lock; + defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator(); + } + } + + virtual bool IsDicomAsJsonNeeded() const + { + // Ask the "DICOM-as-JSON" attachment only if sequences are to + // be returned OR if "query_" contains non-main DICOM tags! + + DicomMap withoutSpecialTags; + withoutSpecialTags.Assign(query_); + + // Check out "ComputeCounters()" + withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); + withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); + withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY); + + // Check out "AddAnswer()" + withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); + withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL); + + return (!sequencesToReturn_.empty() || + !withoutSpecialTags.HasOnlyMainDicomTags()); + } + + virtual void MarkAsComplete() + { + answers_.SetComplete(true); + } + + virtual void Visit(const std::string& publicId, + const std::string& instanceId, + const DicomMap& mainDicomTags, + const Json::Value* dicomAsJson) + { + std::unique_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_)); + + AddAnswer(answers_, mainDicomTags, dicomAsJson, + queryAsArray_, sequencesToReturn_, counters.get(), defaultPrivateCreator_, privateCreators_); + } + }; + + + void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, + const DicomMap& input, + const std::list<DicomTag>& sequencesToReturn, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, + ModalityManufacturer manufacturer) + { + MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_find_scp_duration_ms"); + + /** + * Possibly apply the user-supplied Lua filter. + **/ + + DicomMap lua; + const DicomMap* filteredInput = &input; + + if (ApplyLuaFilter(lua, input, remoteIp, remoteAet, calledAet, manufacturer)) + { + filteredInput = &lua; + } + + + /** + * Retrieve the query level. + **/ + + assert(filteredInput != NULL); + const DicomValue* levelTmp = filteredInput->TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); + if (levelTmp == NULL || + levelTmp->IsNull() || + levelTmp->IsBinary()) + { + throw OrthancException(ErrorCode_BadRequest, + "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)"); + } + + ResourceType level = StringToResourceType(levelTmp->GetContent().c_str()); + + if (level != ResourceType_Patient && + level != ResourceType_Study && + level != ResourceType_Series && + level != ResourceType_Instance) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + + DicomArray query(*filteredInput); + LOG(INFO) << "DICOM C-Find request at level: " << EnumerationToString(level); + + for (size_t i = 0; i < query.GetSize(); i++) + { + if (!query.GetElement(i).GetValue().IsNull()) + { + LOG(INFO) << " " << query.GetElement(i).GetTag() + << " " << FromDcmtkBridge::GetTagName(query.GetElement(i)) + << " = " << query.GetElement(i).GetValue().GetContent(); + } + } + + for (std::list<DicomTag>::const_iterator it = sequencesToReturn.begin(); + it != sequencesToReturn.end(); ++it) + { + LOG(INFO) << " (" << it->Format() + << ") " << FromDcmtkBridge::GetTagName(*it, "") + << " : sequence tag whose content will be copied"; + } + + // collect the private creators from the query itself + std::map<uint16_t, std::string> privateCreators; + for (size_t i = 0; i < query.GetSize(); i++) + { + const DicomElement& element = query.GetElement(i); + if (element.GetTag().IsPrivate() && element.GetTag().GetElement() == 0x10) + { + privateCreators[element.GetTag().GetGroup()] = element.GetValue().GetContent(); + } + } + + /** + * Build up the query object. + **/ + + DatabaseLookup lookup; + + bool caseSensitivePN; + + { + OrthancConfiguration::ReaderLock lock; + caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false); + } + + for (size_t i = 0; i < query.GetSize(); i++) + { + const DicomElement& element = query.GetElement(i); + const DicomTag tag = element.GetTag(); + + if (element.GetValue().IsNull() || + tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL || + tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + continue; + } + + std::string value = element.GetValue().GetContent(); + if (value.size() == 0) + { + // An empty string corresponds to an universal constraint, so we ignore it + continue; + } + + if (FilterQueryTag(value, level, tag, manufacturer)) + { + ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); + + // DICOM specifies that searches must be case sensitive, except + // for tags with a PN value representation + bool sensitive = true; + if (vr == ValueRepresentation_PersonName) + { + sensitive = caseSensitivePN; + } + + lookup.AddDicomConstraint(tag, value, sensitive, true /* mandatory */); + } + else + { + LOG(INFO) << "Because of a patch for the manufacturer of the remote modality, " + << "ignoring constraint on tag (" << tag.Format() << ") " << FromDcmtkBridge::GetTagName(element); + } + } + + + /** + * Run the query. + **/ + + size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_; + + + LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators); + context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit); + } + + + void OrthancFindRequestHandler::FormatOrigin(Json::Value& origin, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, + ModalityManufacturer manufacturer) + { + origin = Json::objectValue; + origin["RemoteIp"] = remoteIp; + origin["RemoteAet"] = remoteAet; + origin["CalledAet"] = calledAet; + origin["Manufacturer"] = EnumerationToString(manufacturer); + } +}