This is an automated email from the git hooks/post-receive script. jodogne-guest pushed a commit to branch master in repository orthanc.
commit c66c8cc4566b3e33c9db99e08a4829805ade0455 Author: jodogne-guest <s.jodo...@gmail.com> Date: Wed Dec 16 13:24:09 2015 +0100 Imported Upstream version 1.0.0+dfsg --- .hg_archival.txt | 6 +- CMakeLists.txt | 2 +- Core/HttpServer/HttpOutput.cpp | 70 +++-- Core/HttpServer/HttpOutput.h | 12 +- NEWS | 11 + OrthancServer/DicomDirWriter.cpp | 331 +++++++++------------ .../DicomProtocol/RemoteModalityParameters.cpp | 13 +- OrthancServer/OrthancFindRequestHandler.cpp | 84 +++++- OrthancServer/OrthancFindRequestHandler.h | 6 + OrthancServer/OrthancInitialization.cpp | 59 ++-- OrthancServer/Search/HierarchicalMatcher.cpp | 12 +- OrthancServer/Search/RangeConstraint.cpp | 11 +- Plugins/Engine/OrthancPlugins.cpp | 47 ++- Plugins/Engine/OrthancPlugins.h | 4 + Plugins/Include/orthanc/OrthancCPlugin.h | 62 +++- Plugins/Samples/ModalityWorklists/Plugin.cpp | 7 +- Resources/CMake/DcmtkConfiguration.cmake | 6 +- .../Samples/Lua/IncomingFindRequestFilter.lua | 14 + 18 files changed, 498 insertions(+), 259 deletions(-) diff --git a/.hg_archival.txt b/.hg_archival.txt index 2cc1f3b..6473729 100644 --- a/.hg_archival.txt +++ b/.hg_archival.txt @@ -1,5 +1,5 @@ repo: 3959d33612ccaadc0d4d707227fbed09ac35e5fe -node: b4e8a031b0d86caf89e1a99fb7b86bb94d00c6a8 -branch: Orthanc-0.9.6 +node: 74cf1f350b45473e847fed4116c1b428fa3d7352 +branch: Orthanc-1.0.0 latesttag: null -latesttagdistance: 1612 +latesttagdistance: 1626 diff --git a/CMakeLists.txt b/CMakeLists.txt index 2737e27..7f6ae71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 2.8) project(Orthanc) # Version of the build, should always be "mainline" except in release branches -set(ORTHANC_VERSION "0.9.6") +set(ORTHANC_VERSION "1.0.0") # Version of the database schema. History: # * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning diff --git a/Core/HttpServer/HttpOutput.cpp b/Core/HttpServer/HttpOutput.cpp index d28ae80..b749e90 100644 --- a/Core/HttpServer/HttpOutput.cpp +++ b/Core/HttpServer/HttpOutput.cpp @@ -449,12 +449,59 @@ namespace Orthanc } - void HttpOutput::StateMachine::SendMultipartItem(const void* item, size_t length) + void HttpOutput::StateMachine::SendMultipartItem(const void* item, + size_t length, + const std::map<std::string, std::string>& headers) { + if (state_ != State_WritingMultipart) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + std::string header = "--" + multipartBoundary_ + "\r\n"; - header += "Content-Type: " + multipartContentType_ + "\r\n"; - header += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\r\n"; - header += "MIME-Version: 1.0\r\n\r\n"; + + bool hasContentType = false; + bool hasContentLength = false; + bool hasMimeVersion = false; + + for (std::map<std::string, std::string>::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + header += it->first + ": " + it->second + "\r\n"; + + std::string tmp; + Toolbox::ToLowerCase(tmp, it->first); + + if (tmp == "content-type") + { + hasContentType = true; + } + + if (tmp == "content-length") + { + hasContentLength = true; + } + + if (tmp == "mime-version") + { + hasMimeVersion = true; + } + } + + if (!hasContentType) + { + header += "Content-Type: " + multipartContentType_ + "\r\n"; + } + + if (!hasContentLength) + { + header += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\r\n"; + } + + if (!hasMimeVersion) + { + header += "MIME-Version: 1.0\r\n\r\n"; + } stream_.Send(false, header.c_str(), header.size()); @@ -463,7 +510,7 @@ namespace Orthanc stream_.Send(false, item, length); } - stream_.Send(false, "\r\n", 2); + stream_.Send(false, "\r\n", 2); } @@ -489,19 +536,6 @@ namespace Orthanc } - void HttpOutput::SendMultipartItem(const std::string& item) - { - if (item.size() > 0) - { - stateMachine_.SendMultipartItem(item.c_str(), item.size()); - } - else - { - stateMachine_.SendMultipartItem(NULL, 0); - } - } - - void HttpOutput::Answer(IHttpStreamAnswer& stream) { HttpCompression compression = stream.SetupHttpCompression(isGzipAllowed_, isDeflateAllowed_); diff --git a/Core/HttpServer/HttpOutput.h b/Core/HttpServer/HttpOutput.h index 370d441..8e2386b 100644 --- a/Core/HttpServer/HttpOutput.h +++ b/Core/HttpServer/HttpOutput.h @@ -99,7 +99,9 @@ namespace Orthanc void StartMultipart(const std::string& subType, const std::string& contentType); - void SendMultipartItem(const void* item, size_t length); + void SendMultipartItem(const void* item, + size_t length, + const std::map<std::string, std::string>& headers); void CloseMultipart(); @@ -202,11 +204,11 @@ namespace Orthanc stateMachine_.StartMultipart(subType, contentType); } - void SendMultipartItem(const std::string& item); - - void SendMultipartItem(const void* item, size_t size) + void SendMultipartItem(const void* item, + size_t size, + const std::map<std::string, std::string>& headers) { - stateMachine_.SendMultipartItem(item, size); + stateMachine_.SendMultipartItem(item, size, headers); } void CloseMultipart() diff --git a/NEWS b/NEWS index b762a16..6120d99 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,17 @@ Pending changes in the mainline =============================== +Version 1.0.0 (2015/12/15) +========================== + +* Lua: "IncomingFindRequestFilter()" to apply filters to incoming C-Find requests +* New function in plugin SDK: "OrthancPluginSendMultipartItem2()" +* Fix of DICOMDIR generation with DCMTK 3.6.1, support of encodings +* Fix range search if the lower or upper limit is absent +* Fix modality worklists lookups if tags with UN (unknown) VR are present +* Warn about badly formatted modality/peer definitions in configuration file at startup + + Version 0.9.6 (2015/12/08) ========================== diff --git a/OrthancServer/DicomDirWriter.cpp b/OrthancServer/DicomDirWriter.cpp index b5edb85..a7d838d 100644 --- a/OrthancServer/DicomDirWriter.cpp +++ b/OrthancServer/DicomDirWriter.cpp @@ -134,173 +134,107 @@ namespace Orthanc Index index_; - /******************************************************************************* - * Functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0 - *******************************************************************************/ - - // print an error message to the console (stderr) that something went wrong with an attribute - static void printAttributeErrorMessage(const DcmTagKey &key, - const OFCondition &error, - const char *operation) + DcmDicomDir& GetDicomDir() { - if (error.bad()) + if (dir_.get() == NULL) { - OFString str; - if (operation != NULL) - { - str = "cannot "; - str += operation; - str += " "; - } - LOG(ERROR) << error.text() << ": " << str << DcmTag(key).getTagName() << " " << key; + dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), + fileSetId_.c_str())); + //SetTagValue(dir_->getRootRecord(), DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(Encoding_Utf8)); } - } - // copy element from dataset to directory record - static void copyElement(DcmItem& dataset, - const DcmTagKey &key, - DcmDirectoryRecord& record, - const OFBool optional, - const OFBool copyEmpty) - { - /* check whether tag exists in source dataset (if optional) */ - if (!optional || (copyEmpty && dataset.tagExists(key)) || dataset.tagExistsWithValue(key)) - { - DcmElement *delem = NULL; - /* get copy of element from source dataset */ - OFCondition status = dataset.findAndGetElement(key, delem, OFFalse /*searchIntoSub*/, OFTrue /*createCopy*/); - if (status.good()) - { - /* ... and insert it into the destination dataset (record) */ - status = record.insert(delem, OFTrue /*replaceOld*/); - if (status.good()) - { - DcmTag tag(key); - /* check for correct VR in the dataset */ - if (delem->getVR() != tag.getEVR()) - { - /* create warning message */ - LOG(WARNING) << "DICOMDIR: possibly wrong VR: " - << tag.getTagName() << " " << key << " with " - << DcmVR(delem->getVR()).getVRName() << " found, expected " - << tag.getVRName() << " instead"; - } - } else - delete delem; - } else if (status == EC_TagNotFound) - status = record.insertEmptyElement(key); - printAttributeErrorMessage(key, status, "insert"); - } + return *dir_; } - // copy optional string value from dataset to directory record - static void copyStringWithDefault(DcmItem& dataset, - const DcmTagKey &key, - DcmDirectoryRecord& record, - const char *defaultValue, - const OFBool printWarning) + + DcmDirectoryRecord& GetRoot() { - OFCondition status; - if (dataset.tagExistsWithValue(key)) - { - OFString stringValue; - /* retrieve string value from source dataset and put it into the destination dataset */ - status = dataset.findAndGetOFStringArray(key, stringValue); - if (status.good()) - status = record.putAndInsertString(key, stringValue.c_str()); - } else { - if (printWarning && (defaultValue != NULL)) - { - /* create warning message */ - LOG(WARNING) << "DICOMDIR: " << DcmTag(key).getTagName() << " " - << key << " missing, using alternative: " << defaultValue; - } - /* put default value */ - status = record.putAndInsertString(key, defaultValue); - } + return GetDicomDir().getRootRecord(); } - // create alternative study date if absent in dataset - static OFString &alternativeStudyDate(DcmItem& dataset, - OFString &result) + + static bool GetUtf8TagValue(std::string& result, + DcmItem& source, + Encoding encoding, + const DcmTagKey& key) { - /* use another date if present */ - if (dataset.findAndGetOFStringArray(DCM_SeriesDate, result).bad() || result.empty()) + DcmElement* element = NULL; + + if (source.findAndGetElement(key, element).good()) { - if (dataset.findAndGetOFStringArray(DCM_AcquisitionDate, result).bad() || result.empty()) + char* s = NULL; + if (element->isLeaf() && + element->getString(s).good() && + s != NULL) { - if (dataset.findAndGetOFStringArray(DCM_ContentDate, result).bad() || result.empty()) - { - /* use current date, "19000101" in case of error */ - DcmDate::getCurrentDate(result); - } + result = Toolbox::ConvertToUtf8(s, encoding); + return true; } } - return result; + + result.clear(); + return false; } - // create alternative study time if absent in dataset - static OFString &alternativeStudyTime(DcmItem& dataset, - OFString &result) + static void SetTagValue(DcmDirectoryRecord& target, + const DcmTagKey& key, + const std::string& valueUtf8) { - /* use another time if present */ - if (dataset.findAndGetOFStringArray(DCM_SeriesTime, result).bad() || result.empty()) + std::string s = Toolbox::ConvertFromUtf8(valueUtf8, Encoding_Ascii); + + if (!target.putAndInsertString(key, s.c_str()).good()) { - if (dataset.findAndGetOFStringArray(DCM_AcquisitionTime, result).bad() || result.empty()) - { - if (dataset.findAndGetOFStringArray(DCM_ContentTime, result).bad() || result.empty()) - { - /* use current time, "0000" in case of error */ - DcmTime::getCurrentTime(result); - } - } + throw OrthancException(ErrorCode_InternalError); } - return result; } + - static void copyElementType1(DcmItem& dataset, - const DcmTagKey &key, - DcmDirectoryRecord& record) + static bool CopyString(DcmDirectoryRecord& target, + DcmDataset& source, + Encoding encoding, + const DcmTagKey& key, + bool optional, + bool copyEmpty) { - copyElement(dataset, key, record, OFFalse /*optional*/, OFFalse /*copyEmpty*/); - } + if (optional && + !source.tagExistsWithValue(key) && + !(copyEmpty && source.tagExists(key))) + { + return false; + } - static void copyElementType1C(DcmItem& dataset, - const DcmTagKey &key, - DcmDirectoryRecord& record) - { - copyElement(dataset, key, record, OFTrue /*optional*/, OFFalse /*copyEmpty*/); - } + std::string value; + bool found = GetUtf8TagValue(value, source, encoding, key); - static void copyElementType2(DcmItem& dataset, - const DcmTagKey &key, - DcmDirectoryRecord& record) - { - copyElement(dataset, key, record, OFFalse /*optional*/, OFTrue /*copyEmpty*/); + SetTagValue(target, key, value); + return found; } - /******************************************************************************* - * End of functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0 - *******************************************************************************/ - - DcmDicomDir& GetDicomDir() + static void CopyStringType1(DcmDirectoryRecord& target, + DcmDataset& source, + Encoding encoding, + const DcmTagKey& key) { - if (dir_.get() == NULL) - { - dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), - fileSetId_.c_str())); - } - - return *dir_; + CopyString(target, source, encoding, key, false, false); } + static void CopyStringType1C(DcmDirectoryRecord& target, + DcmDataset& source, + Encoding encoding, + const DcmTagKey& key) + { + CopyString(target, source, encoding, key, true, false); + } - DcmDirectoryRecord& GetRoot() + static void CopyStringType2(DcmDirectoryRecord& target, + DcmDataset& source, + Encoding encoding, + const DcmTagKey& key) { - return GetDicomDir().getRootRecord(); + CopyString(target, source, encoding, key, false, true); } @@ -310,109 +244,127 @@ namespace Orthanc } void FillPatient(DcmDirectoryRecord& record, - DcmItem& dicom) + DcmDataset& dicom, + Encoding encoding) { // cf. "DicomDirInterface::buildPatientRecord()" - copyElementType1C(dicom, DCM_PatientID, record); - copyElementType2(dicom, DCM_PatientName, record); + CopyStringType1C(record, dicom, encoding, DCM_PatientID); + CopyStringType2(record, dicom, encoding, DCM_PatientName); } void FillStudy(DcmDirectoryRecord& record, - DcmItem& dicom) + DcmDataset& dicom, + Encoding encoding) { // cf. "DicomDirInterface::buildStudyRecord()" - OFString tmpString; + std::string nowDate, nowTime; + Toolbox::GetNowDicom(nowDate, nowTime); + + std::string studyDate; + if (!GetUtf8TagValue(studyDate, dicom, encoding, DCM_StudyDate) && + !GetUtf8TagValue(studyDate, dicom, encoding, DCM_SeriesDate) && + !GetUtf8TagValue(studyDate, dicom, encoding, DCM_AcquisitionDate) && + !GetUtf8TagValue(studyDate, dicom, encoding, DCM_ContentDate)) + { + studyDate = nowDate; + } + + std::string studyTime; + if (!GetUtf8TagValue(studyTime, dicom, encoding, DCM_StudyTime) && + !GetUtf8TagValue(studyTime, dicom, encoding, DCM_SeriesTime) && + !GetUtf8TagValue(studyTime, dicom, encoding, DCM_AcquisitionTime) && + !GetUtf8TagValue(studyTime, dicom, encoding, DCM_ContentTime)) + { + studyTime = nowTime; + } + /* copy attribute values from dataset to study record */ - copyStringWithDefault(dicom, DCM_StudyDate, record, - alternativeStudyDate(dicom, tmpString).c_str(), OFTrue /*printWarning*/); - copyStringWithDefault(dicom, DCM_StudyTime, record, - alternativeStudyTime(dicom, tmpString).c_str(), OFTrue /*printWarning*/); - copyElementType2(dicom, DCM_StudyDescription, record); - copyElementType1(dicom, DCM_StudyInstanceUID, record); + SetTagValue(record, DCM_StudyDate, studyDate); + SetTagValue(record, DCM_StudyTime, studyTime); + CopyStringType2(record, dicom, encoding, DCM_StudyDescription); + CopyStringType1(record, dicom, encoding, DCM_StudyInstanceUID); /* use type 1C instead of 1 in order to avoid unwanted overwriting */ - copyElementType1C(dicom, DCM_StudyID, record); - copyElementType2(dicom, DCM_AccessionNumber, record); + CopyStringType1C(record, dicom, encoding, DCM_StudyID); + CopyStringType2(record, dicom, encoding, DCM_AccessionNumber); } void FillSeries(DcmDirectoryRecord& record, - DcmItem& dicom) + DcmDataset& dicom, + Encoding encoding) { // cf. "DicomDirInterface::buildSeriesRecord()" /* copy attribute values from dataset to series record */ - copyElementType1(dicom, DCM_Modality, record); - copyElementType1(dicom, DCM_SeriesInstanceUID, record); + CopyStringType1(record, dicom, encoding, DCM_Modality); + CopyStringType1(record, dicom, encoding, DCM_SeriesInstanceUID); /* use type 1C instead of 1 in order to avoid unwanted overwriting */ - copyElementType1C(dicom, DCM_SeriesNumber, record); + CopyStringType1C(record, dicom, encoding, DCM_SeriesNumber); } void FillInstance(DcmDirectoryRecord& record, - DcmItem& dicom, + DcmDataset& dicom, + Encoding encoding, DcmMetaInfo& metaInfo, const char* path) { // cf. "DicomDirInterface::buildImageRecord()" /* copy attribute values from dataset to image record */ - copyElementType1(dicom, DCM_InstanceNumber, record); - //copyElementType1C(dicom, DCM_ImageType, record); - copyElementType1C(dicom, DCM_ReferencedImageSequence, record); - - OFString tmp; + CopyStringType1(record, dicom, encoding, DCM_InstanceNumber); + //CopyElementType1C(record, dicom, encoding, DCM_ImageType); - DcmElement* item = record.remove(DCM_ReferencedImageSequence); - if (item != NULL) - { - delete item; - } + // REMOVED since 0.9.7: copyElementType1C(dicom, DCM_ReferencedImageSequence, record); - if (record.putAndInsertString(DCM_ReferencedFileID, path).bad() || - dicom.findAndGetOFStringArray(DCM_SOPClassUID, tmp).bad() || - record.putAndInsertString(DCM_ReferencedSOPClassUIDInFile, tmp.c_str()).bad() || - dicom.findAndGetOFStringArray(DCM_SOPInstanceUID, tmp).bad() || - record.putAndInsertString(DCM_ReferencedSOPInstanceUIDInFile, tmp.c_str()).bad() || - metaInfo.findAndGetOFStringArray(DCM_TransferSyntaxUID, tmp).bad() || - record.putAndInsertString(DCM_ReferencedTransferSyntaxUIDInFile, tmp.c_str()).bad()) + std::string sopClassUid, sopInstanceUid, transferSyntaxUid; + if (!GetUtf8TagValue(sopClassUid, dicom, encoding, DCM_SOPClassUID) || + !GetUtf8TagValue(sopInstanceUid, dicom, encoding, DCM_SOPInstanceUID) || + !GetUtf8TagValue(transferSyntaxUid, metaInfo, encoding, DCM_TransferSyntaxUID)) { throw OrthancException(ErrorCode_BadFileFormat); } + + SetTagValue(record, DCM_ReferencedFileID, path); + SetTagValue(record, DCM_ReferencedSOPClassUIDInFile, sopClassUid); + SetTagValue(record, DCM_ReferencedSOPInstanceUIDInFile, sopInstanceUid); + SetTagValue(record, DCM_ReferencedTransferSyntaxUIDInFile, transferSyntaxUid); } bool CreateResource(DcmDirectoryRecord*& target, ResourceType level, - DcmFileFormat& dicom, + ParsedDicomFile& dicom, const char* filename, const char* path) { - DcmDataset& dataset = *dicom.getDataset(); + DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + Encoding encoding = dicom.GetEncoding(); - OFCondition result; - OFString id; + bool found; + std::string id; E_DirRecType type; switch (level) { case ResourceType_Patient: - result = dataset.findAndGetOFString(DCM_PatientID, id); + found = GetUtf8TagValue(id, dataset, encoding, DCM_PatientID); type = ERT_Patient; break; case ResourceType_Study: - result = dataset.findAndGetOFString(DCM_StudyInstanceUID, id); + found = GetUtf8TagValue(id, dataset, encoding, DCM_StudyInstanceUID); type = ERT_Study; break; case ResourceType_Series: - result = dataset.findAndGetOFString(DCM_SeriesInstanceUID, id); + found = GetUtf8TagValue(id, dataset, encoding, DCM_SeriesInstanceUID); type = ERT_Series; break; case ResourceType_Instance: - result = dataset.findAndGetOFString(DCM_SOPInstanceUID, id); + found = GetUtf8TagValue(id, dataset, encoding, DCM_SOPInstanceUID); type = ERT_Image; break; @@ -420,9 +372,9 @@ namespace Orthanc throw OrthancException(ErrorCode_InternalError); } - if (!result.good()) + if (!found) { - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_BadFileFormat); } IndexKey key = std::make_pair(level, std::string(id.c_str())); @@ -439,29 +391,26 @@ namespace Orthanc switch (level) { case ResourceType_Patient: - FillPatient(*record, dataset); + FillPatient(*record, dataset, encoding); break; case ResourceType_Study: - FillStudy(*record, dataset); + FillStudy(*record, dataset, encoding); break; case ResourceType_Series: - FillSeries(*record, dataset); + FillSeries(*record, dataset, encoding); break; case ResourceType_Instance: - FillInstance(*record, dataset, *dicom.getMetaInfo(), path); + FillInstance(*record, dataset, encoding, *dicom.GetDcmtkObject().getMetaInfo(), path); break; default: throw OrthancException(ErrorCode_InternalError); } - if (record->isAffectedBySpecificCharacterSet()) - { - copyElementType1C(dataset, DCM_SpecificCharacterSet, *record); - } + CopyStringType1C(*record, dataset, encoding, DCM_SpecificCharacterSet); target = record.get(); GetRoot().insertSub(record.release()); @@ -527,26 +476,24 @@ namespace Orthanc path = directory + '\\' + filename; } - DcmFileFormat& fileFormat = dicom.GetDcmtkObject(); - DcmDirectoryRecord* instance; - bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str()); + bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, dicom, filename.c_str(), path.c_str()); if (isNewInstance) { DcmDirectoryRecord* series; - bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, fileFormat, filename.c_str(), NULL); + bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, dicom, filename.c_str(), NULL); series->insertSub(instance); if (isNewSeries) { DcmDirectoryRecord* study; - bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, fileFormat, filename.c_str(), NULL); + bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, dicom, filename.c_str(), NULL); study->insertSub(series); if (isNewStudy) { DcmDirectoryRecord* patient; - pimpl_->CreateResource(patient, ResourceType_Patient, fileFormat, filename.c_str(), NULL); + pimpl_->CreateResource(patient, ResourceType_Patient, dicom, filename.c_str(), NULL); patient->insertSub(study); } } diff --git a/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp b/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp index efb8182..1338f33 100644 --- a/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp +++ b/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp @@ -33,6 +33,7 @@ #include "../PrecompiledHeadersServer.h" #include "RemoteModalityParameters.h" +#include "../../Core/Logging.h" #include "../../Core/OrthancException.h" #include <boost/lexical_cast.hpp> @@ -97,7 +98,17 @@ namespace Orthanc if (modality.size() == 4) { - SetManufacturer(modality.get(3u, "").asString()); + const std::string& manufacturer = modality.get(3u, "").asString(); + + try + { + SetManufacturer(manufacturer); + } + catch (OrthancException&) + { + LOG(ERROR) << "Unknown modality manufacturer: \"" << manufacturer << "\""; + throw; + } } else { diff --git a/OrthancServer/OrthancFindRequestHandler.cpp b/OrthancServer/OrthancFindRequestHandler.cpp index 27b0832..6844336 100644 --- a/OrthancServer/OrthancFindRequestHandler.cpp +++ b/OrthancServer/OrthancFindRequestHandler.cpp @@ -34,6 +34,7 @@ #include "OrthancFindRequestHandler.h" #include "../Core/DicomFormat/DicomArray.h" +#include "../Core/Lua/LuaFunctionCall.h" #include "../Core/Logging.h" #include "FromDcmtkBridge.h" #include "OrthancInitialization.h" @@ -159,6 +160,71 @@ namespace Orthanc } + bool OrthancFindRequestHandler::ApplyLuaFilter(DicomMap& target, + const DicomMap& source, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + Json::Value output; + + { + LuaScripting::Locker locker(context_.GetLua()); + static const char* NAME = "IncomingFindRequestFilter"; + + if (!locker.GetLua().IsExistingFunction(NAME)) + { + return false; + } + + Json::Value tmp = Json::objectValue; + DicomArray a(source); + + for (size_t i = 0; i < a.GetSize(); i++) + { + const DicomValue& v = a.GetElement(i).GetValue(); + std::string s = (v.IsNull() || v.IsBinary()) ? "" : v.GetContent(); + tmp[a.GetElement(i).GetTag().Format()] = s; + } + + Json::Value origin = Json::objectValue; + origin["RemoteIp"] = remoteIp; + origin["RemoteAet"] = remoteAet; + origin["CalledAet"] = calledAet; + + LuaFunctionCall call(locker.GetLua(), NAME); + call.PushJson(tmp); + call.PushJson(origin); + + call.ExecuteToJson(output, true); + } + + // The Lua context is released at this point + + if (output.type() != Json::objectValue) + { + LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table"; + throw OrthancException(ErrorCode_LuaBadOutput); + } + + Json::Value::Members members = output.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + if (output[members[i]].type() != Json::stringValue) + { + LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table mapping names of DICOM tags to strings"; + throw OrthancException(ErrorCode_LuaBadOutput); + } + + DicomTag tag(FromDcmtkBridge::ParseTag(members[i])); + target.SetValue(tag, output[members[i]].asString()); + } + + return true; + } + + void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, const DicomMap& input, const std::list<DicomTag>& sequencesToReturn, @@ -181,10 +247,24 @@ namespace Orthanc /** + * Possibly apply the user-supplied Lua filter. + **/ + + DicomMap lua; + const DicomMap* filteredInput = &input; + + if (ApplyLuaFilter(lua, input, remoteIp, remoteAet, calledAet)) + { + filteredInput = &lua; + } + + + /** * Retrieve the query level. **/ - const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); + assert(filteredInput != NULL); + const DicomValue* levelTmp = filteredInput->TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); if (levelTmp == NULL || levelTmp->IsNull() || levelTmp->IsBinary()) @@ -204,7 +284,7 @@ namespace Orthanc } - DicomArray query(input); + DicomArray query(*filteredInput); LOG(INFO) << "DICOM C-Find request at level: " << EnumerationToString(level); for (size_t i = 0; i < query.GetSize(); i++) diff --git a/OrthancServer/OrthancFindRequestHandler.h b/OrthancServer/OrthancFindRequestHandler.h index b44cef9..601e5d7 100644 --- a/OrthancServer/OrthancFindRequestHandler.h +++ b/OrthancServer/OrthancFindRequestHandler.h @@ -52,6 +52,12 @@ namespace Orthanc const DicomTag& tag, ModalityManufacturer manufacturer); + bool ApplyLuaFilter(DicomMap& target, + const DicomMap& source, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet); + public: OrthancFindRequestHandler(ServerContext& context) : context_(context), diff --git a/OrthancServer/OrthancInitialization.cpp b/OrthancServer/OrthancInitialization.cpp index a273ad5..a28be68 100644 --- a/OrthancServer/OrthancInitialization.cpp +++ b/OrthancServer/OrthancInitialization.cpp @@ -71,7 +71,7 @@ namespace Orthanc { - static boost::mutex globalMutex_; + static boost::recursive_mutex globalMutex_; static Json::Value configuration_; static boost::filesystem::path defaultDirectory_; static std::string configurationAbsolutePath_; @@ -243,6 +243,26 @@ namespace Orthanc } + static void ValidateGlobalConfiguration() + { + std::set<std::string> ids; + + Configuration::GetListOfOrthancPeers(ids); + for (std::set<std::string>::const_iterator it = ids.begin(); it != ids.end(); ++it) + { + OrthancPeerParameters peer; + Configuration::GetOrthancPeer(peer, *it); + } + + Configuration::GetListOfDicomModalities(ids); + for (std::set<std::string>::const_iterator it = ids.begin(); it != ids.end(); ++it) + { + RemoteModalityParameters modality; + Configuration::GetDicomModalityUsingSymbolicName(modality, *it); + } + } + + static void RegisterUserMetadata() { if (configuration_.isMember("UserMetadata")) @@ -367,7 +387,7 @@ namespace Orthanc void OrthancInitialize(const char* configurationFile) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); #if ORTHANC_SSL_ENABLED == 1 // https://wiki.openssl.org/index.php/Library_Initialization @@ -385,6 +405,7 @@ namespace Orthanc // Read the user-provided configuration ReadGlobalConfiguration(configurationFile); + ValidateGlobalConfiguration(); HttpClient::GlobalInitialize(GetGlobalBoolParameterInternal("HttpsVerifyPeers", true), GetGlobalStringParameterInternal("HttpsCACertificates", "")); @@ -412,7 +433,7 @@ namespace Orthanc void OrthancFinalize() { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); HttpClient::GlobalFinalize(); #if ORTHANC_JPEG_LOSSLESS_ENABLED == 1 @@ -444,7 +465,7 @@ namespace Orthanc std::string Configuration::GetGlobalStringParameter(const std::string& parameter, const std::string& defaultValue) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); return GetGlobalStringParameterInternal(parameter, defaultValue); } @@ -452,7 +473,7 @@ namespace Orthanc int Configuration::GetGlobalIntegerParameter(const std::string& parameter, int defaultValue) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); if (configuration_.isMember(parameter)) { @@ -476,7 +497,7 @@ namespace Orthanc bool Configuration::GetGlobalBoolParameter(const std::string& parameter, bool defaultValue) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); return GetGlobalBoolParameterInternal(parameter, defaultValue); } @@ -484,7 +505,7 @@ namespace Orthanc void Configuration::GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality, const std::string& name) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); if (!configuration_.isMember("DicomModalities")) { @@ -517,7 +538,7 @@ namespace Orthanc void Configuration::GetOrthancPeer(OrthancPeerParameters& peer, const std::string& name) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); if (!configuration_.isMember("OrthancPeers")) { @@ -550,7 +571,7 @@ namespace Orthanc const char* parameter, bool onlyAlphanumeric) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); target.clear(); @@ -589,6 +610,8 @@ namespace Orthanc void Configuration::GetListOfDicomModalities(std::set<std::string>& target) { + target.clear(); + if (!ReadKeys(target, "DicomModalities", true)) { LOG(ERROR) << "Only alphanumeric and dash characters are allowed in the names of the modalities"; @@ -599,6 +622,8 @@ namespace Orthanc void Configuration::GetListOfOrthancPeers(std::set<std::string>& target) { + target.clear(); + if (!ReadKeys(target, "OrthancPeers", true)) { LOG(ERROR) << "Only alphanumeric and dash characters are allowed in the names of Orthanc peers"; @@ -610,7 +635,7 @@ namespace Orthanc void Configuration::SetupRegisteredUsers(MongooseServer& httpServer) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); httpServer.ClearUsers(); @@ -664,7 +689,7 @@ namespace Orthanc std::string Configuration::InterpretStringParameterAsPath(const std::string& parameter) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); return InterpretRelativePath(defaultDirectory_.string(), parameter); } @@ -672,7 +697,7 @@ namespace Orthanc void Configuration::GetGlobalListOfStringsParameter(std::list<std::string>& target, const std::string& key) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); target.clear(); @@ -777,7 +802,7 @@ namespace Orthanc void Configuration::UpdateModality(const std::string& symbolicName, const RemoteModalityParameters& modality) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); if (!configuration_.isMember("DicomModalities")) { @@ -801,7 +826,7 @@ namespace Orthanc void Configuration::RemoveModality(const std::string& symbolicName) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); if (!configuration_.isMember("DicomModalities")) { @@ -823,7 +848,7 @@ namespace Orthanc void Configuration::UpdatePeer(const std::string& symbolicName, const OrthancPeerParameters& peer) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); if (!configuration_.isMember("OrthancPeers")) { @@ -848,7 +873,7 @@ namespace Orthanc void Configuration::RemovePeer(const std::string& symbolicName) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); if (!configuration_.isMember("OrthancPeers")) { @@ -980,7 +1005,7 @@ namespace Orthanc void Configuration::GetConfiguration(Json::Value& result) { - boost::mutex::scoped_lock lock(globalMutex_); + boost::recursive_mutex::scoped_lock lock(globalMutex_); result = configuration_; } diff --git a/OrthancServer/Search/HierarchicalMatcher.cpp b/OrthancServer/Search/HierarchicalMatcher.cpp index 115c9f0..f54e396 100644 --- a/OrthancServer/Search/HierarchicalMatcher.cpp +++ b/OrthancServer/Search/HierarchicalMatcher.cpp @@ -33,6 +33,7 @@ #include "../PrecompiledHeadersServer.h" #include "HierarchicalMatcher.h" +#include "../../Core/Logging.h" #include "../../Core/OrthancException.h" #include "../FromDcmtkBridge.h" #include "../ToDcmtkBridge.h" @@ -125,7 +126,14 @@ namespace Orthanc if (value->IsBinary()) { - throw OrthancException(ErrorCode_BadRequest); + if (!value->GetContent().empty()) + { + LOG(WARNING) << "This C-Find modality worklist query contains a non-empty tag (" + << tag.Format() << ") with UN (unknown) value representation. " + << "It will be ignored."; + } + + constraints_[tag] = NULL; } else if (value->IsNull() || value->GetContent().empty()) @@ -235,7 +243,7 @@ namespace Orthanc if (!item.findAndGetSequence(tag, sequence).good() || sequence == NULL) { - return true; + continue; } bool match = false; diff --git a/OrthancServer/Search/RangeConstraint.cpp b/OrthancServer/Search/RangeConstraint.cpp index f6c1ed0..f629f16 100644 --- a/OrthancServer/Search/RangeConstraint.cpp +++ b/OrthancServer/Search/RangeConstraint.cpp @@ -55,8 +55,15 @@ namespace Orthanc void RangeConstraint::Setup(LookupIdentifierQuery& lookup, const DicomTag& tag) const { - lookup.AddConstraint(tag, IdentifierConstraintType_GreaterOrEqual, lower_); - lookup.AddConstraint(tag, IdentifierConstraintType_SmallerOrEqual, upper_); + if (!lower_.empty()) + { + lookup.AddConstraint(tag, IdentifierConstraintType_GreaterOrEqual, lower_); + } + + if (!upper_.empty()) + { + lookup.AddConstraint(tag, IdentifierConstraintType_SmallerOrEqual, upper_); + } } diff --git a/Plugins/Engine/OrthancPlugins.cpp b/Plugins/Engine/OrthancPlugins.cpp index 39f0816..32e9682 100644 --- a/Plugins/Engine/OrthancPlugins.cpp +++ b/Plugins/Engine/OrthancPlugins.cpp @@ -1574,7 +1574,39 @@ namespace Orthanc *(p.target) = ReturnImage(result); } - + + + void OrthancPlugins::ApplySendMultipartItem(const void* parameters) + { + // An exception might be raised in this function if the + // connection was closed by the HTTP client. + const _OrthancPluginAnswerBuffer& p = + *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters); + + HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output); + + std::map<std::string, std::string> headers; // No custom headers + output->SendMultipartItem(p.answer, p.answerSize, headers); + } + + + void OrthancPlugins::ApplySendMultipartItem2(const void* parameters) + { + // An exception might be raised in this function if the + // connection was closed by the HTTP client. + const _OrthancPluginSendMultipartItem2& p = + *reinterpret_cast<const _OrthancPluginSendMultipartItem2*>(parameters); + HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output); + + std::map<std::string, std::string> headers; + for (uint32_t i = 0; i < p.headersCount; i++) + { + headers[p.headersKeys[i]] = p.headersValues[i]; + } + + output->SendMultipartItem(p.answer, p.answerSize, headers); + } + void OrthancPlugins::DatabaseAnswer(const void* parameters) { @@ -1969,15 +2001,12 @@ namespace Orthanc } case _OrthancPluginService_SendMultipartItem: - { - // An exception might be raised in this function if the - // connection was closed by the HTTP client. - const _OrthancPluginAnswerBuffer& p = - *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters); - HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output); - output->SendMultipartItem(p.answer, p.answerSize); + ApplySendMultipartItem(parameters); + return true; + + case _OrthancPluginService_SendMultipartItem2: + ApplySendMultipartItem2(parameters); return true; - } case _OrthancPluginService_ReadFile: { diff --git a/Plugins/Engine/OrthancPlugins.h b/Plugins/Engine/OrthancPlugins.h index 10ab72a..e535a67 100644 --- a/Plugins/Engine/OrthancPlugins.h +++ b/Plugins/Engine/OrthancPlugins.h @@ -152,6 +152,10 @@ namespace Orthanc void ApplyLookupDictionary(const void* parameters); + void ApplySendMultipartItem(const void* parameters); + + void ApplySendMultipartItem2(const void* parameters); + void ComputeHash(_OrthancPluginService service, const void* parameters); diff --git a/Plugins/Include/orthanc/OrthancCPlugin.h b/Plugins/Include/orthanc/OrthancCPlugin.h index fe40348..36fc61b 100644 --- a/Plugins/Include/orthanc/OrthancCPlugin.h +++ b/Plugins/Include/orthanc/OrthancCPlugin.h @@ -112,9 +112,9 @@ #define ORTHANC_PLUGINS_API #endif -#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 0 -#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 9 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 5 +#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 +#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 0 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 0 @@ -424,6 +424,7 @@ extern "C" _OrthancPluginService_SendMultipartItem = 2009, _OrthancPluginService_SendHttpStatus = 2010, _OrthancPluginService_CompressAndAnswerImage = 2011, + _OrthancPluginService_SendMultipartItem2 = 2012, /* Access to the Orthanc database and API */ _OrthancPluginService_GetDicomForInstance = 3000, @@ -2719,7 +2720,7 @@ extern "C" * @param subType The sub-type of the multipart answer ("mixed" or "related"). * @param contentType The MIME type of the items in the multipart answer. * @return 0 if success, or the error code if failure. - * @see OrthancPluginSendMultipartItem() + * @see OrthancPluginSendMultipartItem(), OrthancPluginSendMultipartItem2() * @ingroup REST **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartMultipartAnswer( @@ -2748,6 +2749,7 @@ extern "C" * @param answerSize Number of bytes of the item. * @return 0 if success, or the error code if failure (this notably happens * if the connection is closed by the client). + * @see OrthancPluginSendMultipartItem2() * @ingroup REST **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem( @@ -4677,6 +4679,58 @@ extern "C" } + + typedef struct + { + OrthancPluginRestOutput* output; + const char* answer; + uint32_t answerSize; + uint32_t headersCount; + const char* const* headersKeys; + const char* const* headersValues; + } _OrthancPluginSendMultipartItem2; + + /** + * @brief Send an item as a part of some HTTP multipart answer, with custom headers. + * + * This function sends an item as a part of some HTTP multipart + * answer that was initiated by OrthancPluginStartMultipartAnswer(). In addition to + * OrthancPluginSendMultipartItem(), this function will set HTTP header associated + * with the item. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param answer Pointer to the memory buffer containing the item. + * @param answerSize Number of bytes of the item. + * @param headersCount The number of HTTP headers. + * @param headersKeys Array containing the keys of the HTTP headers. + * @param headersValues Array containing the values of the HTTP headers. + * @return 0 if success, or the error code if failure (this notably happens + * if the connection is closed by the client). + * @see OrthancPluginSendMultipartItem() + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem2( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* answer, + uint32_t answerSize, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues) + { + _OrthancPluginSendMultipartItem2 params; + params.output = output; + params.answer = answer; + params.answerSize = answerSize; + params.headersCount = headersCount; + params.headersKeys = headersKeys; + params.headersValues = headersValues; + + return context->InvokeService(context, _OrthancPluginService_SendMultipartItem2, ¶ms); + } + + #ifdef __cplusplus } #endif diff --git a/Plugins/Samples/ModalityWorklists/Plugin.cpp b/Plugins/Samples/ModalityWorklists/Plugin.cpp index 7178d95..4840f31 100644 --- a/Plugins/Samples/ModalityWorklists/Plugin.cpp +++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp @@ -131,8 +131,11 @@ OrthancPluginErrorCode Callback(OrthancPluginWorklistAnswers* answers, return OrthancPluginErrorCode_InternalError; } - std::cout << "Received worklist query from remote modality " << remoteAet - << ":" << std::endl << json.toStyledString(); + { + std::string msg = ("Received worklist query from remote modality " + + std::string(remoteAet) + ":\n" + json.toStyledString()); + OrthancPluginLogInfo(context_, msg.c_str()); + } boost::filesystem::path source(folder_); boost::filesystem::directory_iterator end; diff --git a/Resources/CMake/DcmtkConfiguration.cmake b/Resources/CMake/DcmtkConfiguration.cmake index 64007a2..4bc5ce2 100644 --- a/Resources/CMake/DcmtkConfiguration.cmake +++ b/Resources/CMake/DcmtkConfiguration.cmake @@ -191,7 +191,7 @@ else() elseif (EXISTS "${DCMTK_DIR}/config/osconfig.h") # This is for Arch Linux set(DCMTK_CONFIGURATION_FILE "${DCMTK_DIR}/config/osconfig.h") else() - message(FATAL_ERROR "Please install libdcmtk1-dev") + message(FATAL_ERROR "Please install libdcmtk*-dev") endif() # Autodetection of the version of DCMTK @@ -220,8 +220,12 @@ if (NOT DCMTK_USE_EMBEDDED_DICTIONARIES) if (DCMTK_DICTIONARY_DIR STREQUAL "") find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic /usr/share/dcmtk + /usr/share/libdcmtk1 /usr/share/libdcmtk2 + /usr/share/libdcmtk3 /usr/share/libdcmtk4 + /usr/share/libdcmtk5 + /usr/share/libdcmtk6 /usr/local/share/dcmtk ) diff --git a/Resources/Samples/Lua/IncomingFindRequestFilter.lua b/Resources/Samples/Lua/IncomingFindRequestFilter.lua new file mode 100644 index 0000000..cb5955d --- /dev/null +++ b/Resources/Samples/Lua/IncomingFindRequestFilter.lua @@ -0,0 +1,14 @@ +-- This example solves the following problem: +-- https://groups.google.com/d/msg/orthanc-users/PLWKqVVaXLs/n_0x4vKhAgAJ + +function IncomingFindRequestFilter(source, origin) + -- First display the content of the C-Find query + PrintRecursive(source) + PrintRecursive(origin) + + -- Remove the "PrivateCreator" tag from the query + local v = source + v['5555,0010'] = nil + + return v +end -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/orthanc.git _______________________________________________ debian-med-commit mailing list debian-med-commit@lists.alioth.debian.org http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-med-commit