# HG changeset patch # User Sebastien Jodogne # Date 1448034601 -3600 # Node ID bdef349f742cdbe4189c987454506a69d3e2a141 # Parent 9a3a77d1d2a20fe28d82156099c70f4722a39dee# Parent d093f998a83b48c981631843ed169b20df71a2c4 integration worklists->default diff -r 9a3a77d1d2a2 -r bdef349f742c CMakeLists.txt --- a/CMakeLists.txt Thu Nov 19 11:57:32 2015 +0100 +++ b/CMakeLists.txt Fri Nov 20 16:50:01 2015 +0100 @@ -179,6 +179,8 @@ OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/ParsedDicomFile.cpp OrthancServer/QueryRetrieveHandler.cpp + OrthancServer/Search/HierarchicalMatcher.cpp + OrthancServer/Search/IFindConstraint.cpp OrthancServer/Search/LookupIdentifierQuery.cpp OrthancServer/Search/LookupResource.cpp OrthancServer/Search/SetOfResources.cpp diff -r 9a3a77d1d2a2 -r bdef349f742c Core/Enumerations.cpp --- a/Core/Enumerations.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/Core/Enumerations.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -325,6 +325,9 @@ case ErrorCode_CannotOrderSlices: return "Unable to order the slices of the series"; + case ErrorCode_NoWorklistHandler: + return "No request handler factory for DICOM C-Find Modality SCP"; + default: if (error >= ErrorCode_START_PLUGINS) { diff -r 9a3a77d1d2a2 -r bdef349f742c Core/Enumerations.h --- a/Core/Enumerations.h Thu Nov 19 11:57:32 2015 +0100 +++ b/Core/Enumerations.h Fri Nov 20 16:50:01 2015 +0100 @@ -138,6 +138,7 @@ ErrorCode_DatabaseNotInitialized = 2038 /*!< Plugin trying to call the database during its initialization */, ErrorCode_SslDisabled = 2039 /*!< Orthanc has been built without SSL support */, ErrorCode_CannotOrderSlices = 2040 /*!< Unable to order the slices of the series */, + ErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, ErrorCode_START_PLUGINS = 1000000 }; diff -r 9a3a77d1d2a2 -r bdef349f742c NEWS --- a/NEWS Thu Nov 19 11:57:32 2015 +0100 +++ b/NEWS Fri Nov 20 16:50:01 2015 +0100 @@ -18,6 +18,7 @@ Plugins ------- +* New functions "OrthancPluginDicomInstanceToJson()" and "OrthancPluginDicomBufferToJson()" * New function "OrthancPluginRegisterErrorCode()" to declare custom error codes * New function "OrthancPluginRegisterDictionaryTag()" to declare custom DICOM tags * New function "OrthancPluginRestApiGet2()" to provide HTTP headers when calling Orthanc API diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomDirWriter.cpp --- a/OrthancServer/DicomDirWriter.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomDirWriter.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -527,7 +527,7 @@ path = directory + '\\' + filename; } - DcmFileFormat& fileFormat = *reinterpret_cast(dicom.GetDcmtkObject()); + DcmFileFormat& fileFormat = dicom.GetDcmtkObject(); DcmDirectoryRecord* instance; bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str()); diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomInstanceToStore.cpp --- a/OrthancServer/DicomInstanceToStore.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomInstanceToStore.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -41,12 +41,6 @@ namespace Orthanc { - static DcmDataset& GetDataset(ParsedDicomFile& file) - { - return *reinterpret_cast(file.GetDcmtkObject())->getDataset(); - } - - void DicomInstanceToStore::AddMetadata(ResourceType level, MetadataType metadata, const std::string& value) @@ -75,7 +69,8 @@ { // Serialize the parsed DICOM file buffer_.Allocate(); - if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), GetDataset(parsed_.GetContent()))) + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), + *parsed_.GetContent().GetDcmtkObject().getDataset())) { LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer"; throw OrthancException(ErrorCode_InternalError); @@ -103,14 +98,15 @@ if (!summary_.HasContent()) { summary_.Allocate(); - FromDcmtkBridge::Convert(summary_.GetContent(), GetDataset(parsed_.GetContent())); + FromDcmtkBridge::Convert(summary_.GetContent(), + *parsed_.GetContent().GetDcmtkObject().getDataset()); } if (!json_.HasContent()) { json_.Allocate(); - FromDcmtkBridge::ToJson(json_.GetContent(), - GetDataset(parsed_.GetContent()), + FromDcmtkBridge::ToJson(json_.GetContent(), + *parsed_.GetContent().GetDcmtkObject().getDataset(), DicomToJsonFormat_Full, DicomToJsonFlags_Default, 256 /* max string length */); diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/DicomFindAnswers.cpp --- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -34,25 +34,165 @@ #include "DicomFindAnswers.h" #include "../FromDcmtkBridge.h" +#include "../ToDcmtkBridge.h" +#include "../../Core/OrthancException.h" + +#include +#include + namespace Orthanc { + class DicomFindAnswers::Answer + { + private: + ParsedDicomFile* dicom_; + DicomMap* map_; + + void CleanupDicom() + { + if (dicom_ != NULL) + { + dicom_->Remove(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID); + dicom_->Remove(DICOM_TAG_SOP_INSTANCE_UID); + } + } + + public: + Answer(ParsedDicomFile& dicom) : + dicom_(dicom.Clone()), + map_(NULL) + { + CleanupDicom(); + } + + Answer(const char* dicom, + size_t size) : + dicom_(new ParsedDicomFile(dicom, size)), + map_(NULL) + { + CleanupDicom(); + } + + Answer(const DicomMap& map) : + dicom_(NULL), + map_(map.Clone()) + { + } + + ~Answer() + { + if (dicom_ != NULL) + { + delete dicom_; + } + + if (map_ != NULL) + { + delete map_; + } + } + + ParsedDicomFile& GetDicomFile() + { + if (dicom_ == NULL) + { + assert(map_ != NULL); + dicom_ = new ParsedDicomFile(*map_); + } + + return *dicom_; + } + + DcmDataset* ExtractDcmDataset() const + { + if (dicom_ != NULL) + { + return new DcmDataset(*dicom_->GetDcmtkObject().getDataset()); + } + else + { + assert(map_ != NULL); + return ToDcmtkBridge::Convert(*map_); + } + } + }; + + void DicomFindAnswers::Clear() { - for (size_t i = 0; i < items_.size(); i++) + for (size_t i = 0; i < answers_.size(); i++) { - delete items_[i]; + assert(answers_[i] != NULL); + delete answers_[i]; + } + + answers_.clear(); + } + + + void DicomFindAnswers::Reserve(size_t size) + { + if (size > answers_.size()) + { + answers_.reserve(size); } } - void DicomFindAnswers::Reserve(size_t size) + + void DicomFindAnswers::Add(const DicomMap& map) + { + answers_.push_back(new Answer(map)); + } + + + void DicomFindAnswers::Add(ParsedDicomFile& dicom) { - if (size > items_.size()) + answers_.push_back(new Answer(dicom)); + } + + + void DicomFindAnswers::Add(const char* dicom, + size_t size) + { + answers_.push_back(new Answer(dicom, size)); + } + + + DicomFindAnswers::Answer& DicomFindAnswers::GetAnswerInternal(size_t index) const + { + if (index < answers_.size()) { - items_.reserve(size); + return *answers_.at(index); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + ParsedDicomFile& DicomFindAnswers::GetAnswer(size_t index) const + { + return GetAnswerInternal(index).GetDicomFile(); + } + + + DcmDataset* DicomFindAnswers::ExtractDcmDataset(size_t index) const + { + return GetAnswerInternal(index).ExtractDcmDataset(); + } + + + void DicomFindAnswers::ToJson(Json::Value& target, + size_t index, + bool simplify) const + { + DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Simple : DicomToJsonFormat_Full); + GetAnswer(index).ToJson(target, format, DicomToJsonFlags_None, 0); + } + + void DicomFindAnswers::ToJson(Json::Value& target, bool simplify) const { @@ -60,8 +200,8 @@ for (size_t i = 0; i < GetSize(); i++) { - Json::Value answer(Json::objectValue); - FromDcmtkBridge::ToJson(answer, GetAnswer(i), simplify); + Json::Value answer; + ToJson(answer, i, simplify); target.append(answer); } } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/DicomFindAnswers.h --- a/OrthancServer/DicomProtocol/DicomFindAnswers.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h Fri Nov 20 16:50:01 2015 +0100 @@ -32,19 +32,25 @@ #pragma once -#include "../../Core/DicomFormat/DicomMap.h" - -#include -#include +#include "../ParsedDicomFile.h" namespace Orthanc { - class DicomFindAnswers + class DicomFindAnswers : public boost::noncopyable { private: - std::vector items_; + class Answer; + + std::vector answers_; + bool complete_; + + Answer& GetAnswerInternal(size_t index) const; public: + DicomFindAnswers() : complete_(true) + { + } + ~DicomFindAnswers() { Clear(); @@ -54,22 +60,37 @@ void Reserve(size_t index); - void Add(const DicomMap& map) - { - items_.push_back(map.Clone()); - } + void Add(const DicomMap& map); + + void Add(ParsedDicomFile& dicom); + + void Add(const char* dicom, + size_t size); size_t GetSize() const { - return items_.size(); + return answers_.size(); } - const DicomMap& GetAnswer(size_t index) const - { - return *items_.at(index); - } + ParsedDicomFile& GetAnswer(size_t index) const; + + DcmDataset* ExtractDcmDataset(size_t index) const; void ToJson(Json::Value& target, bool simplify) const; + + void ToJson(Json::Value& target, + size_t index, + bool simplify) const; + + bool IsComplete() const + { + return complete_; + } + + void SetComplete(bool isComplete) + { + complete_ = isComplete; + } }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/DicomServer.cpp --- a/OrthancServer/DicomProtocol/DicomServer.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomServer.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -94,6 +94,7 @@ findRequestHandlerFactory_ = NULL; moveRequestHandlerFactory_ = NULL; storeRequestHandlerFactory_ = NULL; + worklistRequestHandlerFactory_ = NULL; applicationEntityFilter_ = NULL; checkCalledAet_ = true; clientTimeout_ = 30; @@ -245,6 +246,29 @@ } } + void DicomServer::SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& factory) + { + Stop(); + worklistRequestHandlerFactory_ = &factory; + } + + bool DicomServer::HasWorklistRequestHandlerFactory() const + { + return (worklistRequestHandlerFactory_ != NULL); + } + + IWorklistRequestHandlerFactory& DicomServer::GetWorklistRequestHandlerFactory() const + { + if (HasWorklistRequestHandlerFactory()) + { + return *worklistRequestHandlerFactory_; + } + else + { + throw OrthancException(ErrorCode_NoWorklistHandler); + } + } + void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory) { Stop(); diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/DicomServer.h --- a/OrthancServer/DicomProtocol/DicomServer.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomServer.h Fri Nov 20 16:50:01 2015 +0100 @@ -35,6 +35,7 @@ #include "IFindRequestHandlerFactory.h" #include "IMoveRequestHandlerFactory.h" #include "IStoreRequestHandlerFactory.h" +#include "IWorklistRequestHandlerFactory.h" #include "IApplicationEntityFilter.h" #include @@ -58,6 +59,7 @@ IFindRequestHandlerFactory* findRequestHandlerFactory_; IMoveRequestHandlerFactory* moveRequestHandlerFactory_; IStoreRequestHandlerFactory* storeRequestHandlerFactory_; + IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_; IApplicationEntityFilter* applicationEntityFilter_; static void ServerThread(DicomServer* server); @@ -91,6 +93,10 @@ bool HasStoreRequestHandlerFactory() const; IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const; + void SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& handler); + bool HasWorklistRequestHandlerFactory() const; + IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const; + void SetApplicationEntityFilter(IApplicationEntityFilter& handler); bool HasApplicationEntityFilter() const; IApplicationEntityFilter& GetApplicationEntityFilter() const; diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/IApplicationEntityFilter.h --- a/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Fri Nov 20 16:50:01 2015 +0100 @@ -38,22 +38,25 @@ namespace Orthanc { - class IApplicationEntityFilter + class IApplicationEntityFilter : public boost::noncopyable { public: virtual ~IApplicationEntityFilter() { } - virtual bool IsAllowedConnection(const std::string& callingIp, - const std::string& callingAet) = 0; + virtual bool IsAllowedConnection(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; - virtual bool IsAllowedRequest(const std::string& callingIp, - const std::string& callingAet, + virtual bool IsAllowedRequest(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, DicomRequestType type) = 0; - virtual bool IsAllowedTransferSyntax(const std::string& callingIp, - const std::string& callingAet, + virtual bool IsAllowedTransferSyntax(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, TransferSyntax syntax) = 0; }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/IFindRequestHandler.h --- a/OrthancServer/DicomProtocol/IFindRequestHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IFindRequestHandler.h Fri Nov 20 16:50:01 2015 +0100 @@ -34,28 +34,19 @@ #include "DicomFindAnswers.h" -#include -#include - - namespace Orthanc { - class IFindRequestHandler + class IFindRequestHandler : public boost::noncopyable { public: virtual ~IFindRequestHandler() { } - /** - * Can throw exceptions. Returns "false" iff too many results have - * to be returned. In such a case, a "Matching terminated due to - * Cancel request" DIMSE code would be returned. - * https://www.dabsoft.ch/dicom/4/V.4.1/ - **/ - virtual bool Handle(DicomFindAnswers& answers, + virtual void Handle(DicomFindAnswers& answers, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet) = 0; + const std::string& remoteAet, + const std::string& calledAet) = 0; }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h --- a/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Fri Nov 20 16:50:01 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class IFindRequestHandlerFactory + class IFindRequestHandlerFactory : public boost::noncopyable { public: virtual ~IFindRequestHandlerFactory() diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/IMoveRequestHandler.h --- a/OrthancServer/DicomProtocol/IMoveRequestHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandler.h Fri Nov 20 16:50:01 2015 +0100 @@ -40,7 +40,7 @@ namespace Orthanc { - class IMoveRequestIterator + class IMoveRequestIterator : public boost::noncopyable { public: enum Status @@ -70,7 +70,8 @@ virtual IMoveRequestIterator* Handle(const std::string& target, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet) = 0; + const std::string& remoteAet, + const std::string& calledAet) = 0; }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h --- a/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Fri Nov 20 16:50:01 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class IMoveRequestHandlerFactory + class IMoveRequestHandlerFactory : public boost::noncopyable { public: virtual ~IMoveRequestHandlerFactory() diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/IStoreRequestHandler.h --- a/OrthancServer/DicomProtocol/IStoreRequestHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandler.h Fri Nov 20 16:50:01 2015 +0100 @@ -40,7 +40,7 @@ namespace Orthanc { - class IStoreRequestHandler + class IStoreRequestHandler : public boost::noncopyable { public: virtual ~IStoreRequestHandler() diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h --- a/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Fri Nov 20 16:50:01 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class IStoreRequestHandlerFactory + class IStoreRequestHandlerFactory : public boost::noncopyable { public: virtual ~IStoreRequestHandlerFactory() diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/IWorklistRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomProtocol/IWorklistRequestHandler.h Fri Nov 20 16:50:01 2015 +0100 @@ -0,0 +1,52 @@ +/** + * 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 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 . + **/ + + +#pragma once + +#include "DicomFindAnswers.h" + +namespace Orthanc +{ + class IWorklistRequestHandler : public boost::noncopyable + { + public: + virtual ~IWorklistRequestHandler() + { + } + + virtual void Handle(DicomFindAnswers& answers, + ParsedDicomFile& query, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; + }; +} diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/DicomProtocol/IWorklistRequestHandlerFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomProtocol/IWorklistRequestHandlerFactory.h Fri Nov 20 16:50:01 2015 +0100 @@ -0,0 +1,48 @@ +/** + * 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 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 . + **/ + + +#pragma once + +#include "IWorklistRequestHandler.h" + +namespace Orthanc +{ + class IWorklistRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IWorklistRequestHandlerFactory() + { + } + + virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() = 0; + }; +} diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/FromDcmtkBridge.cpp --- a/OrthancServer/FromDcmtkBridge.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/FromDcmtkBridge.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -1073,6 +1073,9 @@ case EVR_TM: return ValueRepresentation_Time; + case EVR_SQ: + return ValueRepresentation_Sequence; + default: return ValueRepresentation_Other; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Internals/CommandDispatcher.cpp --- a/OrthancServer/Internals/CommandDispatcher.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/CommandDispatcher.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -430,6 +430,11 @@ knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); } + if (server.HasWorklistRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel); + } + // For C-MOVE if (server.HasMoveRequestHandlerFactory()) { @@ -460,17 +465,17 @@ } // Retrieve the AET and the IP address of the remote modality - std::string callingAet; - std::string callingIp; + std::string remoteAet; + std::string remoteIp; std::string calledAet; { - DIC_AE callingAet_C; + DIC_AE remoteAet_C; DIC_AE calledAet_C; - DIC_AE callingIp_C; + DIC_AE remoteIp_C; DIC_AE calledIP_C; - if (ASC_getAPTitles(assoc->params, callingAet_C, calledAet_C, NULL).bad() || - ASC_getPresentationAddresses(assoc->params, callingIp_C, calledIP_C).bad()) + if (ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() || + ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad()) { T_ASC_RejectParameters rej = { @@ -483,13 +488,13 @@ return NULL; } - callingIp = std::string(/*OFSTRING_GUARD*/(callingIp_C)); - callingAet = std::string(/*OFSTRING_GUARD*/(callingAet_C)); + remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C)); + remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C)); calledAet = (/*OFSTRING_GUARD*/(calledAet_C)); } - LOG(INFO) << "Association Received from AET " << callingAet - << " on IP " << callingIp; + LOG(INFO) << "Association Received from AET " << remoteAet + << " on IP " << remoteIp; std::vector transferSyntaxes; @@ -501,13 +506,13 @@ // New transfer syntaxes supported since Orthanc 0.7.2 if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Deflated)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated)) { transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg)) { transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax); transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax); @@ -532,14 +537,14 @@ } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg2000)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000)) { transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_JpegLossless)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless)) { transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); @@ -548,21 +553,21 @@ } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpip)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip)) { transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Mpeg2)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2)) { transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax); transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax); } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Rle)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle)) { transferSyntaxes.push_back(UID_RLELosslessTransferSyntax); } @@ -638,9 +643,9 @@ } if (server.HasApplicationEntityFilter() && - !server.GetApplicationEntityFilter().IsAllowedConnection(callingIp, callingAet)) + !server.GetApplicationEntityFilter().IsAllowedConnection(remoteIp, remoteAet, calledAet)) { - LOG(WARNING) << "Rejected association for remote AET " << callingAet << " on IP " << callingIp; + LOG(WARNING) << "Rejected association for remote AET " << remoteAet << " on IP " << remoteIp; T_ASC_RejectParameters rej = { ASC_RESULT_REJECTEDPERMANENT, @@ -687,7 +692,7 @@ } IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL; - return new CommandDispatcher(server, assoc, callingIp, callingAet, filter); + return new CommandDispatcher(server, assoc, remoteIp, remoteAet, calledAet, filter); } bool CommandDispatcher::Step() @@ -771,7 +776,7 @@ if (supported && request != DicomRequestType_Echo && // Always allow incoming ECHO requests filter_ != NULL && - !filter_->IsAllowedRequest(remoteIp_, remoteAet_, request)) + !filter_->IsAllowedRequest(remoteIp_, remoteAet_, calledAet_, request)) { LOG(ERROR) << EnumerationToString(request) << " requests are disallowed for the AET \"" @@ -798,7 +803,11 @@ { std::auto_ptr handler (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler()); - cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_); + + if (handler.get() != NULL) + { + cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_); + } } break; @@ -807,16 +816,32 @@ { std::auto_ptr handler (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler()); - cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_); + + if (handler.get() != NULL) + { + cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_); + } } break; case DicomRequestType_Find: - if (server_.HasFindRequestHandlerFactory()) // Should always be true + if (server_.HasFindRequestHandlerFactory() || // Should always be true + server_.HasWorklistRequestHandlerFactory()) { - std::auto_ptr handler - (server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler()); - cond = Internals::findScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_); + std::auto_ptr findHandler; + if (server_.HasFindRequestHandlerFactory()) + { + findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler()); + } + + std::auto_ptr worklistHandler; + if (server_.HasWorklistRequestHandlerFactory()) + { + worklistHandler.reset(server_.GetWorklistRequestHandlerFactory().ConstructWorklistRequestHandler()); + } + + cond = Internals::findScp(assoc_, &msg, presID, findHandler.get(), + worklistHandler.get(), remoteIp_, remoteAet_, calledAet_); } break; diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Internals/CommandDispatcher.h --- a/OrthancServer/Internals/CommandDispatcher.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/CommandDispatcher.h Fri Nov 20 16:50:01 2015 +0100 @@ -52,6 +52,7 @@ T_ASC_Association* assoc_; std::string remoteIp_; std::string remoteAet_; + std::string calledAet_; IApplicationEntityFilter* filter_; public: @@ -59,11 +60,13 @@ T_ASC_Association* assoc, const std::string& remoteIp, const std::string& remoteAet, + const std::string& calledAet, IApplicationEntityFilter* filter) : server_(server), assoc_(assoc), remoteIp_(remoteIp), remoteAet_(remoteAet), + calledAet_(calledAet), filter_(filter) { clientTimeout_ = server.GetClientTimeout(); diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Internals/FindScp.cpp --- a/OrthancServer/Internals/FindScp.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/FindScp.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -87,7 +87,7 @@ #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" - +#include namespace Orthanc { @@ -95,13 +95,13 @@ { struct FindScpData { - IFindRequestHandler* handler_; - DicomMap input_; + IFindRequestHandler* findHandler_; + IWorklistRequestHandler* worklistHandler_; DicomFindAnswers answers_; DcmDataset* lastRequest_; const std::string* remoteIp_; const std::string* remoteAet_; - bool noCroppingOfResults_; + const std::string* calledAet_; }; @@ -120,20 +120,55 @@ bzero(response, sizeof(T_DIMSE_C_FindRSP)); *statusDetail = NULL; + std::string sopClassUid(request->AffectedSOPClassUID); + FindScpData& data = *reinterpret_cast(callbackData); if (data.lastRequest_ == NULL) { - FromDcmtkBridge::Convert(data.input_, *requestIdentifiers); + bool ok = false; try { - data.noCroppingOfResults_ = data.handler_->Handle(data.answers_, data.input_, - *data.remoteIp_, *data.remoteAet_); + if (sopClassUid == UID_FINDModalityWorklistInformationModel) + { + if (data.worklistHandler_ != NULL) + { + ParsedDicomFile query(*requestIdentifiers); + data.worklistHandler_->Handle(data.answers_, query, + *data.remoteIp_, *data.remoteAet_, + *data.calledAet_); + ok = true; + } + else + { + LOG(ERROR) << "No worklist handler is installed, cannot handle this C-FIND request"; + } + } + else + { + if (data.findHandler_ != NULL) + { + DicomMap input; + FromDcmtkBridge::Convert(input, *requestIdentifiers); + data.findHandler_->Handle(data.answers_, input, + *data.remoteIp_, *data.remoteAet_, + *data.calledAet_); + ok = true; + } + else + { + LOG(ERROR) << "No C-Find handler is installed, cannot handle this request"; + } + } } catch (OrthancException& e) { // Internal error! LOG(ERROR) << "C-FIND request handler has failed: " << e.What(); + } + + if (!ok) + { response->DimseStatus = STATUS_FIND_Failed_UnableToProcess; *responseIdentifiers = NULL; return; @@ -153,9 +188,9 @@ { // There are pending results that are still to be sent response->DimseStatus = STATUS_Pending; - *responseIdentifiers = ToDcmtkBridge::Convert(data.answers_.GetAnswer(responseCount - 1)); + *responseIdentifiers = data.answers_.ExtractDcmDataset(responseCount - 1); } - else if (data.noCroppingOfResults_) + else if (data.answers_.IsComplete()) { // Success: All the results have been sent response->DimseStatus = STATUS_Success; @@ -175,16 +210,19 @@ OFCondition Internals::findScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID, - IFindRequestHandler& handler, + IFindRequestHandler* findHandler, + IWorklistRequestHandler* worklistHandler, const std::string& remoteIp, - const std::string& remoteAet) + const std::string& remoteAet, + const std::string& calledAet) { FindScpData data; data.lastRequest_ = NULL; - data.handler_ = &handler; + data.findHandler_ = findHandler; + data.worklistHandler_ = worklistHandler; data.remoteIp_ = &remoteIp; data.remoteAet_ = &remoteAet; - data.noCroppingOfResults_ = true; + data.calledAet_ = &calledAet; OFCondition cond = DIMSE_findProvider(assoc, presID, &msg->msg.CFindRQ, FindScpCallback, &data, diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Internals/FindScp.h --- a/OrthancServer/Internals/FindScp.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/FindScp.h Fri Nov 20 16:50:01 2015 +0100 @@ -33,6 +33,7 @@ #pragma once #include "../DicomProtocol/IFindRequestHandler.h" +#include "../DicomProtocol/IWorklistRequestHandler.h" #include @@ -43,8 +44,10 @@ OFCondition findScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID, - IFindRequestHandler& handler, + IFindRequestHandler* findHandler, // can be NULL + IWorklistRequestHandler* worklistHandler, // can be NULL const std::string& remoteIp, - const std::string& remoteAet); + const std::string& remoteAet, + const std::string& calledAet); } } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Internals/MoveScp.cpp --- a/OrthancServer/Internals/MoveScp.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/MoveScp.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -98,7 +98,6 @@ { std::string target_; IMoveRequestHandler* handler_; - DicomMap input_; DcmDataset* lastRequest_; unsigned int subOperationCount_; unsigned int failureCount_; @@ -106,6 +105,7 @@ std::auto_ptr iterator_; const std::string* remoteIp_; const std::string* remoteAet_; + const std::string* calledAet_; }; @@ -128,12 +128,14 @@ MoveScpData& data = *reinterpret_cast(callbackData); if (data.lastRequest_ == NULL) { - FromDcmtkBridge::Convert(data.input_, *requestIdentifiers); + DicomMap input; + FromDcmtkBridge::Convert(input, *requestIdentifiers); try { - data.iterator_.reset(data.handler_->Handle(data.target_, data.input_, - *data.remoteIp_, *data.remoteAet_)); + data.iterator_.reset(data.handler_->Handle(data.target_, input, + *data.remoteIp_, *data.remoteAet_, + *data.calledAet_)); if (data.iterator_.get() == NULL) { @@ -215,7 +217,8 @@ T_ASC_PresentationContextID presID, IMoveRequestHandler& handler, const std::string& remoteIp, - const std::string& remoteAet) + const std::string& remoteAet, + const std::string& calledAet) { MoveScpData data; data.target_ = std::string(msg->msg.CMoveRQ.MoveDestination); @@ -223,6 +226,7 @@ data.handler_ = &handler; data.remoteIp_ = &remoteIp; data.remoteAet_ = &remoteAet; + data.calledAet_ = &calledAet; OFCondition cond = DIMSE_moveProvider(assoc, presID, &msg->msg.CMoveRQ, MoveScpCallback, &data, diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Internals/MoveScp.h --- a/OrthancServer/Internals/MoveScp.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/MoveScp.h Fri Nov 20 16:50:01 2015 +0100 @@ -45,6 +45,7 @@ T_ASC_PresentationContextID presID, IMoveRequestHandler& handler, const std::string& remoteIp, - const std::string& remoteAet); + const std::string& remoteAet, + const std::string& calledAet); } } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -88,13 +88,14 @@ } - bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, + void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet) + const std::string& remoteAet, + const std::string& calledAet) { /** - * Ensure that the calling modality is known to Orthanc. + * Ensure that the remote modality is known to Orthanc. **/ RemoteModalityParameters modality; @@ -194,7 +195,7 @@ context_.GetIndex().FindCandidates(resources, instances, finder); assert(resources.size() == instances.size()); - bool finished = true; + bool complete = true; for (size_t i = 0; i < instances.size(); i++) { @@ -206,7 +207,7 @@ if (maxResults != 0 && answers.GetSize() >= maxResults) { - finished = false; + complete = false; break; } else @@ -218,6 +219,6 @@ LOG(INFO) << "Number of matching resources: " << answers.GetSize(); - return finished; + answers.SetComplete(complete); } } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/OrthancFindRequestHandler.h --- a/OrthancServer/OrthancFindRequestHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.h Fri Nov 20 16:50:01 2015 +0100 @@ -55,10 +55,11 @@ { } - virtual bool Handle(DicomFindAnswers& answers, + virtual void Handle(DicomFindAnswers& answers, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet); + const std::string& remoteAet, + const std::string& calledAet); unsigned int GetMaxResults() const { diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -165,7 +165,8 @@ IMoveRequestIterator* OrthancMoveRequestHandler::Handle(const std::string& targetAet, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet) + const std::string& remoteAet, + const std::string& calledAet) { LOG(WARNING) << "Move-SCU request received for AET \"" << targetAet << "\""; diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/OrthancMoveRequestHandler.h --- a/OrthancServer/OrthancMoveRequestHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.h Fri Nov 20 16:50:01 2015 +0100 @@ -54,6 +54,7 @@ virtual IMoveRequestIterator* Handle(const std::string& targetAet, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet); + const std::string& remoteAet, + const std::string& calledAet); }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -280,6 +280,18 @@ } + static void CopyTagIfExists(DicomMap& target, + ParsedDicomFile& source, + const DicomTag& tag) + { + std::string tmp; + if (source.GetTagValue(tmp, tag)) + { + target.SetValue(tag, tmp); + } + } + + static void DicomFind(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); @@ -303,15 +315,16 @@ Json::Value result = Json::arrayValue; for (size_t i = 0; i < patients.GetSize(); i++) { - Json::Value patient(Json::objectValue); - FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i), true); + Json::Value patient; + patients.ToJson(patient, i, true); DicomMap::SetupFindStudyTemplate(m); if (!MergeQueryAndTemplate(m, call.GetBodyData(), call.GetBodySize())) { return; } - m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); + + CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); DicomFindAnswers studies; FindStudy(studies, locker.GetConnection(), m); @@ -321,16 +334,17 @@ // Loop over the found studies for (size_t j = 0; j < studies.GetSize(); j++) { - Json::Value study(Json::objectValue); - FromDcmtkBridge::ToJson(study, studies.GetAnswer(j), true); + Json::Value study; + studies.ToJson(study, j, true); DicomMap::SetupFindSeriesTemplate(m); if (!MergeQueryAndTemplate(m, call.GetBodyData(), call.GetBodySize())) { return; } - m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID); - m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); + + CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_PATIENT_ID); + CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); DicomFindAnswers series; FindSeries(series, locker.GetConnection(), m); @@ -339,8 +353,8 @@ study["Series"] = Json::arrayValue; for (size_t k = 0; k < series.GetSize(); k++) { - Json::Value series2(Json::objectValue); - FromDcmtkBridge::ToJson(series2, series.GetAnswer(k), true); + Json::Value series2; + series.ToJson(series2, k, true); study["Series"].append(series2); } @@ -465,8 +479,13 @@ static void GetQueryOneAnswer(RestApiGetCall& call) { size_t index = boost::lexical_cast(call.GetUriComponent("index", "")); + QueryAccessor query(call); - AnswerDicomMap(call, query->GetAnswer(index), call.HasArgument("simplify")); + + DicomMap map; + query->GetAnswer(map, index); + + AnswerDicomMap(call, map, call.HasArgument("simplify")); } @@ -547,7 +566,9 @@ // Ensure that the answer of interest does exist size_t index = boost::lexical_cast(call.GetUriComponent("index", "")); - query->GetAnswer(index); + + DicomMap map; + query->GetAnswer(map, index); RestApi::AutoListChildren(call); } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/ParsedDicomFile.cpp --- a/OrthancServer/ParsedDicomFile.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/ParsedDicomFile.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -153,7 +153,8 @@ // This method can only be called from the constructors! - void ParsedDicomFile::Setup(const char* buffer, size_t size) + void ParsedDicomFile::Setup(const void* buffer, + size_t size) { DcmInputBufferStream is; if (size > 0) @@ -823,7 +824,20 @@ } - ParsedDicomFile::ParsedDicomFile(const char* content, size_t size) : pimpl_(new PImpl) + ParsedDicomFile::ParsedDicomFile(const DicomMap& map) : pimpl_(new PImpl) + { + std::auto_ptr dataset(ToDcmtkBridge::Convert(map)); + + // NOTE: This implies an unnecessary memory copy of the dataset, but no way to get around + // http://support.dcmtk.org/redmine/issues/544 + std::auto_ptr fileFormat(new DcmFileFormat(dataset.get())); + + pimpl_->file_.reset(fileFormat.release()); + } + + + ParsedDicomFile::ParsedDicomFile(const void* content, + size_t size) : pimpl_(new PImpl) { Setup(content, size); } @@ -851,15 +865,27 @@ } + ParsedDicomFile::ParsedDicomFile(DcmDataset& dicom) : pimpl_(new PImpl) + { + pimpl_->file_.reset(new DcmFileFormat(&dicom)); + } + + + ParsedDicomFile::ParsedDicomFile(DcmFileFormat& dicom) : pimpl_(new PImpl) + { + pimpl_->file_.reset(new DcmFileFormat(dicom)); + } + + ParsedDicomFile::~ParsedDicomFile() { delete pimpl_; } - void* ParsedDicomFile::GetDcmtkObject() + DcmFileFormat& ParsedDicomFile::GetDcmtkObject() { - return pimpl_->file_.get(); + return *pimpl_->file_.get(); } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/ParsedDicomFile.h --- a/OrthancServer/ParsedDicomFile.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/ParsedDicomFile.h Fri Nov 20 16:50:01 2015 +0100 @@ -39,6 +39,9 @@ #include "../Core/Images/ImageBuffer.h" #include "../Core/IDynamicObject.h" +class DcmDataset; +class DcmFileFormat; + namespace Orthanc { class ParsedDicomFile : public IDynamicObject @@ -49,7 +52,7 @@ ParsedDicomFile(ParsedDicomFile& other); - void Setup(const char* content, + void Setup(const void* content, size_t size); void RemovePrivateTagsInternal(const std::set* toKeep); @@ -61,14 +64,20 @@ public: ParsedDicomFile(); // Create a minimal DICOM instance - ParsedDicomFile(const char* content, + ParsedDicomFile(const DicomMap& map); + + ParsedDicomFile(const void* content, size_t size); ParsedDicomFile(const std::string& content); + ParsedDicomFile(DcmDataset& dicom); + + ParsedDicomFile(DcmFileFormat& dicom); + ~ParsedDicomFile(); - void* GetDcmtkObject(); + DcmFileFormat& GetDcmtkObject(); ParsedDicomFile* Clone(); diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/QueryRetrieveHandler.cpp --- a/OrthancServer/QueryRetrieveHandler.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/QueryRetrieveHandler.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -95,31 +95,24 @@ } - const DicomMap& QueryRetrieveHandler::GetAnswer(size_t i) + void QueryRetrieveHandler::GetAnswer(DicomMap& target, + size_t i) { Run(); - - if (i >= answers_.GetSize()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - return answers_.GetAnswer(i); + answers_.GetAnswer(i).Convert(target); } void QueryRetrieveHandler::Retrieve(const std::string& target, size_t i) { - Run(); + DicomMap map; + GetAnswer(map, i); - if (i >= answers_.GetSize()) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); + locker.GetConnection().Move(target, map); } - - ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); - locker.GetConnection().Move(target, answers_.GetAnswer(i)); } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/QueryRetrieveHandler.h --- a/OrthancServer/QueryRetrieveHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/QueryRetrieveHandler.h Fri Nov 20 16:50:01 2015 +0100 @@ -85,7 +85,8 @@ size_t GetAnswerCount(); - const DicomMap& GetAnswer(size_t i); + void GetAnswer(DicomMap& target, + size_t i); void Retrieve(const std::string& target, size_t i); diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/HierarchicalMatcher.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/HierarchicalMatcher.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -0,0 +1,329 @@ +/** + * 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 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 . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "HierarchicalMatcher.h" + +#include "../../Core/OrthancException.h" +#include "../FromDcmtkBridge.h" +#include "../ToDcmtkBridge.h" + +#include + +namespace Orthanc +{ + HierarchicalMatcher::HierarchicalMatcher(ParsedDicomFile& query, + bool caseSensitivePN) + { + Setup(*query.GetDcmtkObject().getDataset(), + caseSensitivePN, + query.GetEncoding()); + } + + + HierarchicalMatcher::~HierarchicalMatcher() + { + for (Constraints::iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + if (it->second != NULL) + { + delete it->second; + } + } + + for (Sequences::iterator it = sequences_.begin(); + it != sequences_.end(); ++it) + { + if (it->second != NULL) + { + delete it->second; + } + } + } + + + void HierarchicalMatcher::Setup(DcmItem& dataset, + bool caseSensitivePN, + Encoding encoding) + { + for (unsigned long i = 0; i < dataset.card(); i++) + { + DcmElement* element = dataset.getElement(i); + if (element == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DicomTag tag(FromDcmtkBridge::Convert(element->getTag())); + if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + // Ignore this specific tag + continue; + } + + ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); + + if (constraints_.find(tag) != constraints_.end() || + sequences_.find(tag) != sequences_.end()) + { + throw OrthancException(ErrorCode_BadRequest); + } + + if (vr == ValueRepresentation_Sequence) + { + DcmSequenceOfItems& sequence = dynamic_cast(*element); + + if (sequence.card() == 0 || + (sequence.card() == 1 && sequence.getItem(0)->card() == 0)) + { + // Universal matching of a sequence + sequences_[tag] = NULL; + } + else if (sequence.card() == 1) + { + sequences_[tag] = new HierarchicalMatcher(*sequence.getItem(0), caseSensitivePN, encoding); + } + else + { + throw OrthancException(ErrorCode_BadRequest); + } + } + else + { + std::auto_ptr value(FromDcmtkBridge::ConvertLeafElement + (*element, DicomToJsonFlags_None, encoding)); + + if (value->IsBinary() || + value->IsNull()) + { + throw OrthancException(ErrorCode_BadRequest); + } + else if (value->GetContent().empty()) + { + // This is an universal matcher + constraints_[tag] = NULL; + } + else + { + // DICOM specifies that searches must be case sensitive, except + // for tags with a PN value representation + bool sensitive = true; + if (vr == ValueRepresentation_PatientName) + { + sensitive = caseSensitivePN; + } + + constraints_[tag] = IFindConstraint::ParseDicomConstraint(tag, value->GetContent(), sensitive); + } + } + } + } + + + std::string HierarchicalMatcher::Format(const std::string& prefix) const + { + std::string s; + + for (Constraints::const_iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + s += prefix + it->first.Format() + " "; + + if (it->second == NULL) + { + s += "*\n"; + } + else + { + s += it->second->Format() + "\n"; + } + } + + for (Sequences::const_iterator it = sequences_.begin(); + it != sequences_.end(); ++it) + { + s += prefix + it->first.Format() + " "; + + if (it->second == NULL) + { + s += "*\n"; + } + else + { + s += "Sequence:\n" + it->second->Format(prefix + " "); + } + } + + return s; + } + + + bool HierarchicalMatcher::Match(ParsedDicomFile& dicom) const + { + return MatchInternal(*dicom.GetDcmtkObject().getDataset(), + dicom.GetEncoding()); + } + + + bool HierarchicalMatcher::MatchInternal(DcmItem& item, + Encoding encoding) const + { + for (Constraints::const_iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + if (it->second != NULL) + { + DcmTagKey tag = ToDcmtkBridge::Convert(it->first); + + DcmElement* element = NULL; + if (!item.findAndGetElement(tag, element).good() || + element == NULL) + { + return false; + } + + std::auto_ptr value(FromDcmtkBridge::ConvertLeafElement + (*element, DicomToJsonFlags_None, encoding)); + + if (value->IsNull() || + value->IsBinary() || + !it->second->Match(value->GetContent())) + { + return false; + } + } + } + + for (Sequences::const_iterator it = sequences_.begin(); + it != sequences_.end(); ++it) + { + if (it->second != NULL) + { + DcmTagKey tag = ToDcmtkBridge::Convert(it->first); + + DcmSequenceOfItems* sequence = NULL; + if (!item.findAndGetSequence(tag, sequence).good() || + sequence == NULL) + { + return true; + } + + bool match = false; + + for (unsigned long i = 0; i < sequence->card(); i++) + { + if (it->second->MatchInternal(*sequence->getItem(i), encoding)) + { + match = true; + break; + } + } + + if (!match) + { + return false; + } + } + } + + return true; + } + + + DcmDataset* HierarchicalMatcher::ExtractInternal(DcmItem& source, + Encoding encoding) const + { + std::auto_ptr target(new DcmDataset); + + for (Constraints::const_iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + DcmTagKey tag = ToDcmtkBridge::Convert(it->first); + + DcmElement* element = NULL; + if (source.findAndGetElement(tag, element).good() && + element != NULL) + { + std::auto_ptr cloned(FromDcmtkBridge::CreateElementForTag(it->first)); + cloned->copyFrom(*element); + target->insert(cloned.release()); + } + } + + for (Sequences::const_iterator it = sequences_.begin(); + it != sequences_.end(); ++it) + { + DcmTagKey tag = ToDcmtkBridge::Convert(it->first); + + DcmSequenceOfItems* sequence = NULL; + if (source.findAndGetSequence(tag, sequence).good() && + sequence != NULL) + { + std::auto_ptr cloned(new DcmSequenceOfItems(tag)); + + for (unsigned long i = 0; i < sequence->card(); i++) + { + if (it->second == NULL) + { + cloned->append(new DcmItem(*sequence->getItem(i))); + } + else if (it->second->MatchInternal(*sequence->getItem(i), encoding)) // TODO Might be optimized + { + // It is necessary to encapsulate the child dataset into a + // "DcmItem" object before it can be included in a + // sequence. Otherwise, "dciodvfy" reports an error "Bad + // tag in sequence - Expecting Item or Sequence Delimiter." + std::auto_ptr child(it->second->ExtractInternal(*sequence->getItem(i), encoding)); + cloned->append(new DcmItem(*child)); + } + } + + target->insert(cloned.release()); + } + } + + return target.release(); + } + + + ParsedDicomFile* HierarchicalMatcher::Extract(ParsedDicomFile& dicom) const + { + std::auto_ptr dataset(ExtractInternal(*dicom.GetDcmtkObject().getDataset(), + dicom.GetEncoding())); + + std::auto_ptr result(new ParsedDicomFile(*dataset)); + result->SetEncoding(Encoding_Utf8); + + return result.release(); + } +} diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/HierarchicalMatcher.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/HierarchicalMatcher.h Fri Nov 20 16:50:01 2015 +0100 @@ -0,0 +1,81 @@ +/** + * 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 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 . + **/ + + +#pragma once + +#include "../../Core/DicomFormat/DicomMap.h" +#include "IFindConstraint.h" +#include "../ParsedDicomFile.h" + +class DcmItem; + +namespace Orthanc +{ + class HierarchicalMatcher : public boost::noncopyable + { + private: + typedef std::map Constraints; + typedef std::map Sequences; + + Constraints constraints_; + Sequences sequences_; + + void Setup(DcmItem& query, + bool caseSensitivePN, + Encoding encoding); + + HierarchicalMatcher(DcmItem& query, + bool caseSensitivePN, + Encoding encoding) + { + Setup(query, caseSensitivePN, encoding); + } + + bool MatchInternal(DcmItem& dicom, + Encoding encoding) const; + + DcmDataset* ExtractInternal(DcmItem& dicom, + Encoding encoding) const; + + public: + HierarchicalMatcher(ParsedDicomFile& query, + bool caseSensitivePN); + + ~HierarchicalMatcher(); + + std::string Format(const std::string& prefix = "") const; + + bool Match(ParsedDicomFile& dicom) const; + + ParsedDicomFile* Extract(ParsedDicomFile& dicom) const; + }; +} diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/IFindConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/IFindConstraint.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -0,0 +1,130 @@ +/** + * 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 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 . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "IFindConstraint.h" + +#include "ListConstraint.h" +#include "RangeConstraint.h" +#include "ValueConstraint.h" +#include "WildcardConstraint.h" + +#include "../FromDcmtkBridge.h" +#include "../../Core/OrthancException.h" + +namespace Orthanc +{ + IFindConstraint* IFindConstraint::ParseDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive) + { + ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); + + if (vr == ValueRepresentation_Sequence) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if ((vr == ValueRepresentation_Date || + vr == ValueRepresentation_DateTime || + vr == ValueRepresentation_Time) && + dicomQuery.find('-') != std::string::npos) + { + /** + * Range matching is only defined for TM, DA and DT value + * representations. This code fixes issues 35 and 37. + * + * Reference: "Range matching is not defined for types of + * Attributes other than dates and times", DICOM PS 3.4, + * C.2.2.2.5 ("Range Matching"). + **/ + size_t separator = dicomQuery.find('-'); + std::string lower = dicomQuery.substr(0, separator); + std::string upper = dicomQuery.substr(separator + 1); + return new RangeConstraint(lower, upper, caseSensitive); + } + else if (dicomQuery.find('\\') != std::string::npos) + { + std::auto_ptr constraint(new ListConstraint(caseSensitive)); + + std::vector items; + Toolbox::TokenizeString(items, dicomQuery, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + constraint->AddAllowedValue(items[i]); + } + + return constraint.release(); + } + else if (dicomQuery.find('*') != std::string::npos || + dicomQuery.find('?') != std::string::npos) + { + return new WildcardConstraint(dicomQuery, caseSensitive); + } + else + { + /** + * Case-insensitive match for PN value representation (Patient + * Name). Case-senstive match for all the other value + * representations. + * + * Reference: DICOM PS 3.4 + * - C.2.2.2.1 ("Single Value Matching") + * - C.2.2.2.4 ("Wild Card Matching") + * http://medical.nema.org/Dicom/2011/11_04pu.pdf + * + * "Except for Attributes with a PN Value Representation, only + * entities with values which match exactly the value specified in the + * request shall match. This matching is case-sensitive, i.e., + * sensitive to the exact encoding of the key attribute value in + * character sets where a letter may have multiple encodings (e.g., + * based on its case, its position in a word, or whether it is + * accented) + * + * For Attributes with a PN Value Representation (e.g., Patient Name + * (0010,0010)), an application may perform literal matching that is + * either case-sensitive, or that is insensitive to some or all + * aspects of case, position, accent, or other character encoding + * variants." + * + * (0008,0018) UI SOPInstanceUID => Case-sensitive + * (0008,0050) SH AccessionNumber => Case-sensitive + * (0010,0020) LO PatientID => Case-sensitive + * (0020,000D) UI StudyInstanceUID => Case-sensitive + * (0020,000E) UI SeriesInstanceUID => Case-sensitive + **/ + + return new ValueConstraint(dicomQuery, caseSensitive); + } + } +} diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/IFindConstraint.h --- a/OrthancServer/Search/IFindConstraint.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/IFindConstraint.h Fri Nov 20 16:50:01 2015 +0100 @@ -49,5 +49,11 @@ const DicomTag& tag) const = 0; virtual bool Match(const std::string& value) const = 0; + + virtual std::string Format() const = 0; + + static IFindConstraint* ParseDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive); }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/ListConstraint.cpp --- a/OrthancServer/Search/ListConstraint.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/ListConstraint.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -75,4 +75,23 @@ return allowedValues_.find(v) != allowedValues_.end(); } + + + std::string ListConstraint::Format() const + { + std::string s; + + for (std::set::const_iterator + it = allowedValues_.begin(); it != allowedValues_.end(); ++it) + { + if (!s.empty()) + { + s += "\\"; + } + + s += *it; + } + + return s; + } } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/ListConstraint.h --- a/OrthancServer/Search/ListConstraint.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/ListConstraint.h Fri Nov 20 16:50:01 2015 +0100 @@ -67,5 +67,7 @@ const DicomTag& tag) const; virtual bool Match(const std::string& value) const; + + virtual std::string Format() const; }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/LookupResource.cpp --- a/OrthancServer/Search/LookupResource.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/LookupResource.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -33,11 +33,6 @@ #include "../PrecompiledHeadersServer.h" #include "LookupResource.h" -#include "ListConstraint.h" -#include "RangeConstraint.h" -#include "ValueConstraint.h" -#include "WildcardConstraint.h" - #include "../../Core/OrthancException.h" #include "../../Core/FileStorage/StorageAccessor.h" #include "../ServerToolbox.h" @@ -426,85 +421,16 @@ const std::string& dicomQuery, bool caseSensitive) { - ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html if (tag == DICOM_TAG_MODALITIES_IN_STUDY) { SetModalitiesInStudy(dicomQuery); } - else if ((vr == ValueRepresentation_Date || - vr == ValueRepresentation_DateTime || - vr == ValueRepresentation_Time) && - dicomQuery.find('-') != std::string::npos) - { - /** - * Range matching is only defined for TM, DA and DT value - * representations. This code fixes issues 35 and 37. - * - * Reference: "Range matching is not defined for types of - * Attributes other than dates and times", DICOM PS 3.4, - * C.2.2.2.5 ("Range Matching"). - **/ - size_t separator = dicomQuery.find('-'); - std::string lower = dicomQuery.substr(0, separator); - std::string upper = dicomQuery.substr(separator + 1); - Add(tag, new RangeConstraint(lower, upper, caseSensitive)); - } - else if (dicomQuery.find('\\') != std::string::npos) - { - std::auto_ptr constraint(new ListConstraint(caseSensitive)); - - std::vector items; - Toolbox::TokenizeString(items, dicomQuery, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - constraint->AddAllowedValue(items[i]); - } - - Add(tag, constraint.release()); - } - else if (dicomQuery.find('*') != std::string::npos || - dicomQuery.find('?') != std::string::npos) + else { - Add(tag, new WildcardConstraint(dicomQuery, caseSensitive)); - } - else - { - /** - * Case-insensitive match for PN value representation (Patient - * Name). Case-senstive match for all the other value - * representations. - * - * Reference: DICOM PS 3.4 - * - C.2.2.2.1 ("Single Value Matching") - * - C.2.2.2.4 ("Wild Card Matching") - * http://medical.nema.org/Dicom/2011/11_04pu.pdf - * - * "Except for Attributes with a PN Value Representation, only - * entities with values which match exactly the value specified in the - * request shall match. This matching is case-sensitive, i.e., - * sensitive to the exact encoding of the key attribute value in - * character sets where a letter may have multiple encodings (e.g., - * based on its case, its position in a word, or whether it is - * accented) - * - * For Attributes with a PN Value Representation (e.g., Patient Name - * (0010,0010)), an application may perform literal matching that is - * either case-sensitive, or that is insensitive to some or all - * aspects of case, position, accent, or other character encoding - * variants." - * - * (0008,0018) UI SOPInstanceUID => Case-sensitive - * (0008,0050) SH AccessionNumber => Case-sensitive - * (0010,0020) LO PatientID => Case-sensitive - * (0020,000D) UI StudyInstanceUID => Case-sensitive - * (0020,000E) UI SeriesInstanceUID => Case-sensitive - **/ - - Add(tag, new ValueConstraint(dicomQuery, caseSensitive)); + Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive)); } } + } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/RangeConstraint.h --- a/OrthancServer/Search/RangeConstraint.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/RangeConstraint.h Fri Nov 20 16:50:01 2015 +0100 @@ -64,5 +64,10 @@ const DicomTag& tag) const; virtual bool Match(const std::string& value) const; + + virtual std::string Format() const + { + return lower_ + "-" + upper_; + } }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/ValueConstraint.h --- a/OrthancServer/Search/ValueConstraint.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/ValueConstraint.h Fri Nov 20 16:50:01 2015 +0100 @@ -61,5 +61,10 @@ const DicomTag& tag) const; virtual bool Match(const std::string& value) const; + + virtual std::string Format() const + { + return value_; + } }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/WildcardConstraint.cpp --- a/OrthancServer/Search/WildcardConstraint.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/WildcardConstraint.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -78,4 +78,9 @@ { lookup.AddConstraint(tag, IdentifierConstraintType_Wildcard, pimpl_->wildcard_); } + + std::string WildcardConstraint::Format() const + { + return pimpl_->wildcard_; + } } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/Search/WildcardConstraint.h --- a/OrthancServer/Search/WildcardConstraint.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/WildcardConstraint.h Fri Nov 20 16:50:01 2015 +0100 @@ -59,5 +59,7 @@ const DicomTag& tag) const; virtual bool Match(const std::string& value) const; + + virtual std::string Format() const; }; } diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/ServerEnumerations.h Fri Nov 20 16:50:01 2015 +0100 @@ -98,7 +98,8 @@ ValueRepresentation_PatientName, ValueRepresentation_Date, ValueRepresentation_DateTime, - ValueRepresentation_Time + ValueRepresentation_Time, + ValueRepresentation_Sequence }; enum DicomToJsonFormat diff -r 9a3a77d1d2a2 -r bdef349f742c OrthancServer/main.cpp --- a/OrthancServer/main.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/main.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -159,14 +159,16 @@ { } - virtual bool IsAllowedConnection(const std::string& /*callingIp*/, - const std::string& /*callingAet*/) + virtual bool IsAllowedConnection(const std::string& /*remoteIp*/, + const std::string& /*remoteAet*/, + const std::string& /*calledAet*/) { return true; } - virtual bool IsAllowedRequest(const std::string& /*callingIp*/, - const std::string& callingAet, + virtual bool IsAllowedRequest(const std::string& /*remoteIp*/, + const std::string& remoteAet, + const std::string& /*calledAet*/, DicomRequestType type) { if (type == DicomRequestType_Store) @@ -175,9 +177,9 @@ return true; } - if (!Configuration::IsKnownAETitle(callingAet)) + if (!Configuration::IsKnownAETitle(remoteAet)) { - LOG(ERROR) << "Unknown remote DICOM modality AET: \"" << callingAet << "\""; + LOG(ERROR) << "Unknown remote DICOM modality AET: \"" << remoteAet << "\""; return false; } else @@ -186,8 +188,9 @@ } } - virtual bool IsAllowedTransferSyntax(const std::string& callingIp, - const std::string& callingAet, + virtual bool IsAllowedTransferSyntax(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& /*calledAet*/, TransferSyntax syntax) { std::string configuration; @@ -234,8 +237,8 @@ if (locker.GetLua().IsExistingFunction(lua.c_str())) { LuaFunctionCall call(locker.GetLua(), lua.c_str()); - call.PushString(callingAet); - call.PushString(callingIp); + call.PushString(remoteAet); + call.PushString(remoteIp); return call.ExecutePredicate(); } } @@ -550,6 +553,7 @@ PrintErrorCode(ErrorCode_DatabaseNotInitialized, "Plugin trying to call the database during its initialization"); PrintErrorCode(ErrorCode_SslDisabled, "Orthanc has been built without SSL support"); PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series"); + PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP"); } std::cout << std::endl; @@ -704,6 +708,14 @@ dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory); dicomServer.SetFindRequestHandlerFactory(serverFactory); + +#if ORTHANC_PLUGINS_ENABLED == 1 + if (plugins) + { + dicomServer.SetWorklistRequestHandlerFactory(*plugins); + } +#endif + dicomServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("DicomPort", 4242)); dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")); dicomServer.SetApplicationEntityFilter(dicomFilter); diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/Plugins/Engine/OrthancPlugins.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -47,6 +47,7 @@ #include "../../OrthancServer/OrthancInitialization.h" #include "../../OrthancServer/ServerContext.h" #include "../../OrthancServer/ServerToolbox.h" +#include "../../OrthancServer/Search/HierarchicalMatcher.h" #include "../../Core/Compression/ZlibCompressor.h" #include "../../Core/Compression/GzipCompressor.h" #include "../../Core/Images/Image.h" @@ -61,6 +62,46 @@ namespace Orthanc { + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + const void* data, + size_t size) + { + target.size = size; + + if (size == 0) + { + target.data = NULL; + } + else + { + target.data = malloc(size); + if (target.data != NULL) + { + memcpy(target.data, data, size); + } + else + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + } + + + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + const std::string& str) + { + if (str.size() == 0) + { + target.size = 0; + target.data = NULL; + } + else + { + CopyToMemoryBuffer(target, str.c_str(), str.size()); + } + } + + namespace { class PluginStorageArea : public IStorageArea @@ -237,6 +278,7 @@ typedef std::list RestCallbacks; typedef std::list OnStoredCallbacks; typedef std::list OnChangeCallbacks; + typedef std::list WorklistCallbacks; typedef std::map Properties; PluginsManager manager_; @@ -244,10 +286,12 @@ RestCallbacks restCallbacks_; OnStoredCallbacks onStoredCallbacks_; OnChangeCallbacks onChangeCallbacks_; + WorklistCallbacks worklistCallbacks_; std::auto_ptr storageArea_; boost::recursive_mutex restCallbackMutex_; boost::recursive_mutex storedCallbackMutex_; boost::recursive_mutex changeCallbackMutex_; + boost::recursive_mutex worklistCallbackMutex_; boost::recursive_mutex invokeServiceMutex_; Properties properties_; int argc_; @@ -265,6 +309,88 @@ + class OrthancPlugins::WorklistHandler : public IWorklistRequestHandler + { + private: + OrthancPlugins& that_; + std::auto_ptr matcher_; + ParsedDicomFile* currentQuery_; + + void Reset() + { + matcher_.reset(NULL); + currentQuery_ = NULL; + } + + public: + WorklistHandler(OrthancPlugins& that) : that_(that) + { + Reset(); + } + + virtual void Handle(DicomFindAnswers& answers, + ParsedDicomFile& query, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + bool caseSensitivePN = Configuration::GetGlobalBoolParameter("CaseSensitivePN", false); + matcher_.reset(new HierarchicalMatcher(query, caseSensitivePN)); + currentQuery_ = &query; + + { + boost::recursive_mutex::scoped_lock lock(that_.pimpl_->worklistCallbackMutex_); + + for (PImpl::WorklistCallbacks::const_iterator + callback = that_.pimpl_->worklistCallbacks_.begin(); + callback != that_.pimpl_->worklistCallbacks_.end(); ++callback) + { + OrthancPluginErrorCode error = (*callback) + (reinterpret_cast(&answers), + reinterpret_cast(this), + remoteAet.c_str(), + calledAet.c_str()); + + if (error != OrthancPluginErrorCode_Success) + { + Reset(); + that_.GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast(error)); + } + } + } + + Reset(); + } + + void GetQueryDicom(OrthancPluginMemoryBuffer& target) const + { + assert(currentQuery_ != NULL); + std::string dicom; + currentQuery_->SaveToMemoryBuffer(dicom); + CopyToMemoryBuffer(target, dicom.c_str(), dicom.size()); + } + + bool IsMatch(const void* dicom, + size_t size) const + { + assert(matcher_.get() != NULL); + ParsedDicomFile f(dicom, size); + return matcher_->Match(f); + } + + void AddAnswer(OrthancPluginWorklistAnswers* answers, + const void* dicom, + size_t size) const + { + assert(matcher_.get() != NULL); + ParsedDicomFile f(dicom, size); + std::auto_ptr summary(matcher_->Extract(f)); + reinterpret_cast(answers)->Add(*summary); + } + }; + + static char* CopyString(const std::string& str) { char *result = reinterpret_cast(malloc(str.size() + 1)); @@ -546,46 +672,6 @@ - static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, - const void* data, - size_t size) - { - target.size = size; - - if (size == 0) - { - target.data = NULL; - } - else - { - target.data = malloc(size); - if (target.data != NULL) - { - memcpy(target.data, data, size); - } - else - { - throw OrthancException(ErrorCode_NotEnoughMemory); - } - } - } - - - static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, - const std::string& str) - { - if (str.size() == 0) - { - target.size = 0; - target.data = NULL; - } - else - { - CopyToMemoryBuffer(target, str.c_str(), str.size()); - } - } - - void OrthancPlugins::RegisterRestCallback(const void* parameters, bool lock) { @@ -622,6 +708,17 @@ } + void OrthancPlugins::RegisterWorklistCallback(const void* parameters) + { + const _OrthancPluginWorklistCallback& p = + *reinterpret_cast(parameters); + + LOG(INFO) << "Plugin has registered an modality worklist callback"; + pimpl_->worklistCallbacks_.push_back(p.callback); + } + + + void OrthancPlugins::AnswerBuffer(const void* parameters) { @@ -1401,6 +1498,10 @@ RegisterOnChangeCallback(parameters); return true; + case _OrthancPluginService_RegisterWorklistCallback: + RegisterWorklistCallback(parameters); + return true; + case _OrthancPluginService_AnswerBuffer: AnswerBuffer(parameters); return true; @@ -1831,6 +1932,38 @@ ApplyDicomToJson(service, parameters); return true; + case _OrthancPluginService_AddWorklistAnswer: + { + const _OrthancPluginWorklistAnswersOperation& p = + *reinterpret_cast(parameters); + reinterpret_cast(p.query)->AddAnswer(p.answers, p.dicom, p.size); + return true; + } + + case _OrthancPluginService_MarkWorklistAnswersIncomplete: + { + const _OrthancPluginWorklistAnswersOperation& p = + *reinterpret_cast(parameters); + reinterpret_cast(p.answers)->SetComplete(false); + return true; + } + + case _OrthancPluginService_IsWorklistMatch: + { + const _OrthancPluginWorklistQueryOperation& p = + *reinterpret_cast(parameters); + *p.isMatch = reinterpret_cast(p.query)->IsMatch(p.dicom, p.size); + return true; + } + + case _OrthancPluginService_GetWorklistQueryDicom: + { + const _OrthancPluginWorklistQueryOperation& p = + *reinterpret_cast(parameters); + reinterpret_cast(p.query)->GetQueryDicom(*p.target); + return true; + } + default: { // This service is unknown to the Orthanc plugin engine @@ -1948,4 +2081,24 @@ { return pimpl_->dictionary_; } + + + IWorklistRequestHandler* OrthancPlugins::ConstructWorklistRequestHandler() + { + bool hasHandler; + + { + boost::recursive_mutex::scoped_lock lock(pimpl_->worklistCallbackMutex_); + hasHandler = !pimpl_->worklistCallbacks_.empty(); + } + + if (hasHandler) + { + return new WorklistHandler(*this); + } + else + { + return NULL; + } + } } diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Engine/OrthancPlugins.h --- a/Plugins/Engine/OrthancPlugins.h Thu Nov 19 11:57:32 2015 +0100 +++ b/Plugins/Engine/OrthancPlugins.h Fri Nov 20 16:50:01 2015 +0100 @@ -50,6 +50,7 @@ #include "../../Core/FileStorage/IStorageArea.h" #include "../../Core/HttpServer/IHttpHandler.h" #include "../../OrthancServer/IServerListener.h" +#include "../../OrthancServer/DicomProtocol/IWorklistRequestHandlerFactory.h" #include "OrthancPluginDatabase.h" #include "PluginsManager.h" @@ -63,12 +64,15 @@ class OrthancPlugins : public IHttpHandler, public IPluginServiceProvider, - public IServerListener + public IServerListener, + public IWorklistRequestHandlerFactory { private: struct PImpl; boost::shared_ptr pimpl_; + class WorklistHandler; + void CheckContextAvailable(); void RegisterRestCallback(const void* parameters, @@ -78,6 +82,8 @@ void RegisterOnChangeCallback(const void* parameters); + void RegisterWorklistCallback(const void* parameters); + void AnswerBuffer(const void* parameters); void Redirect(const void* parameters); @@ -204,6 +210,9 @@ { SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL); } + + + virtual IWorklistRequestHandler* ConstructWorklistRequestHandler(); }; } diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Thu Nov 19 11:57:32 2015 +0100 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Fri Nov 20 16:50:01 2015 +0100 @@ -18,6 +18,7 @@ * - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback(). * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea(). * - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV2(). + * - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback(). * -# void OrthancPluginFinalize(): * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -49,6 +50,9 @@ * @defgroup Callbacks Callbacks * @brief Functions to register and manage callbacks by the plugins. * + * @defgroup Worklists Worklists + * @brief Functions to register and manage worklists. + * * @defgroup Orthanc Orthanc * @brief Functions to access the content of the Orthanc server. **/ @@ -271,6 +275,7 @@ OrthancPluginErrorCode_DatabaseNotInitialized = 2038 /*!< Plugin trying to call the database during its initialization */, OrthancPluginErrorCode_SslDisabled = 2039 /*!< Orthanc has been built without SSL support */, OrthancPluginErrorCode_CannotOrderSlices = 2040 /*!< Unable to order the slices of the series */, + OrthancPluginErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, _OrthancPluginErrorCode_INTERNAL = 0x7fffffff } OrthancPluginErrorCode; @@ -398,6 +403,7 @@ _OrthancPluginService_RegisterStorageArea = 1002, _OrthancPluginService_RegisterOnChangeCallback = 1003, _OrthancPluginService_RegisterRestCallbackNoLock = 1004, + _OrthancPluginService_RegisterWorklistCallback = 1005, /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -462,6 +468,12 @@ _OrthancPluginService_GetFontInfo = 6010, _OrthancPluginService_DrawText = 6011, + /* Primitives for handling worklists */ + _OrthancPluginService_AddWorklistAnswer = 7000, + _OrthancPluginService_MarkWorklistAnswersIncomplete = 7001, + _OrthancPluginService_IsWorklistMatch = 7002, + _OrthancPluginService_GetWorklistQueryDicom = 7003, + _OrthancPluginService_INTERNAL = 0x7fffffff } _OrthancPluginService; @@ -754,6 +766,22 @@ /** + * @brief Opaque structure to an object that represents a C-Find query. + * @ingroup Worklists + **/ + typedef struct _OrthancPluginWorklistQuery_t OrthancPluginWorklistQuery; + + + + /** + * @brief Opaque structure to an object that represents the answers to a C-Find query. + * @ingroup Worklists + **/ + typedef struct _OrthancPluginWorklistAnswers_t OrthancPluginWorklistAnswers; + + + + /** * @brief Signature of a callback function that answers to a REST request. * @ingroup Callbacks **/ @@ -849,6 +877,27 @@ /** + * @brief Callback to handle the C-Find SCP requests received by Orthanc. + * + * Signature of a callback function that is triggered when Orthanc + * receives a C-Find SCP request against modality worklists. + * + * @param answers The target structure where answers must be stored. + * @param query The worklist query. + * @param remoteAet The Application Entity Title (AET) of the modality from which the request originates. + * @param calledAet The Application Entity Title (AET) of the modality that is called by the request. + * @return 0 if success, other value if error. + * @ingroup Worklists + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWorklistCallback) ( + OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const char* remoteAet, + const char* calledAet); + + + + /** * @brief Data structure that contains information about the Orthanc core. **/ typedef struct _OrthancPluginContext_t @@ -4025,6 +4074,182 @@ return context->InvokeService(context, _OrthancPluginService_RestApiGet2, ¶ms); } + + + typedef struct + { + OrthancPluginWorklistCallback callback; + } _OrthancPluginWorklistCallback; + + /** + * @brief Register a callback to handle modality worklists requests. + * + * This function registers a callback to handle C-Find SCP requests + * on modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterWorklistCallback( + OrthancPluginContext* context, + OrthancPluginWorklistCallback callback) + { + _OrthancPluginWorklistCallback params; + params.callback = callback; + + context->InvokeService(context, _OrthancPluginService_RegisterWorklistCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginWorklistAnswers* answers; + const OrthancPluginWorklistQuery* query; + const void* dicom; + uint32_t size; + } _OrthancPluginWorklistAnswersOperation; + + /** + * @brief Add one answer to some modality worklist request. + * + * This function adds one worklist (encoded as a DICOM file) to the + * set of answers corresponding to some C-Find SCP request against + * modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answers The set of answers. + * @param query The worklist query, as received by the callback. + * @param dicom The worklist to answer, encoded as a DICOM file. + * @param size The size of the DICOM file. + * @return 0 if success, other value if error. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginWorklistAddWorklistAnswer( + OrthancPluginContext* context, + OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const void* dicom, + uint32_t size) + { + _OrthancPluginWorklistAnswersOperation params; + params.answers = answers; + params.query = query; + params.dicom = dicom; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_AddWorklistAnswer, ¶ms); + } + + + /** + * @brief Mark the set of worklist answers as incomplete. + * + * This function marks as incomplete the set of answers + * corresponding to some C-Find SCP request against modality + * worklists. This must be used if canceling the handling of a + * request when too many answers are to be returned. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answers The set of answers. + * @return 0 if success, other value if error. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginMarkWorklistAnswersIncomplete( + OrthancPluginContext* context, + OrthancPluginWorklistAnswers* answers) + { + _OrthancPluginWorklistAnswersOperation params; + params.answers = answers; + params.query = NULL; + params.dicom = NULL; + params.size = 0; + + return context->InvokeService(context, _OrthancPluginService_MarkWorklistAnswersIncomplete, ¶ms); + } + + + typedef struct + { + const OrthancPluginWorklistQuery* query; + const void* dicom; + uint32_t size; + int32_t* isMatch; + OrthancPluginMemoryBuffer* target; + } _OrthancPluginWorklistQueryOperation; + + /** + * @brief Test whether a worklist matches the query. + * + * This function checks whether one worklist (encoded as a DICOM + * file) matches the C-Find SCP query against modality + * worklists. This function must be called before adding the + * worklist as an answer through + * OrthancPluginWorklistAddWorklistAnswer(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param query The worklist query, as received by the callback. + * @param dicom The worklist to answer, encoded as a DICOM file. + * @param size The size of the DICOM file. + * @return 1 if the worklist matches the query, 0 otherwise. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginIsWorklistMatch( + OrthancPluginContext* context, + const OrthancPluginWorklistQuery* query, + const void* dicom, + uint32_t size) + { + int32_t isMatch = 0; + + _OrthancPluginWorklistQueryOperation params; + params.query = query; + params.dicom = dicom; + params.size = size; + params.isMatch = &isMatch; + params.target = NULL; + + if (context->InvokeService(context, _OrthancPluginService_IsWorklistMatch, ¶ms) == OrthancPluginErrorCode_Success) + { + return isMatch; + } + else + { + /* Error: Assume non-match */ + return 0; + } + } + + + /** + * @brief Retrieve the worklist query as a DICOM file. + * + * This function retrieves the DICOM file that underlies a C-Find + * SCP query against modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target Memory buffer where to store the DICOM file. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param query The worklist query, as received by the callback. + * @return 0 if success, other value if error. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetWorklistQueryDicom( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const OrthancPluginWorklistQuery* query) + { + _OrthancPluginWorklistQueryOperation params; + params.query = query; + params.dicom = NULL; + params.size = 0; + params.isMatch = NULL; + params.target = target; + + return context->InvokeService(context, _OrthancPluginService_GetWorklistQueryDicom, ¶ms); + } + + #ifdef __cplusplus } #endif diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ModalityWorklists/CMakeLists.txt Fri Nov 20 16:50:01 2015 +0100 @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 2.8) + +project(SampleModalityWorklists) + +SET(SAMPLE_MODALITY_WORKLISTS_VERSION "0.0" CACHE STRING "Version of the plugin") +SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") +SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") + +SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") +SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") + +set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) +include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) + +add_library(SampleModalityWorklists SHARED + Plugin.cpp + ${JSONCPP_SOURCES} + ${BOOST_SOURCES} + ) + +message("Setting the version of the plugin to ${SAMPLE_MODALITY_WORKLISTS_VERSION}") +add_definitions( + -DSAMPLE_MODALITY_WORKLISTS_VERSION="${SAMPLE_MODALITY_WORKLISTS_VERSION}" + -DDEFAULT_WORKLISTS_FOLDER="${CMAKE_SOURCE_DIR}/WorklistsDatabase" + ) + +set_target_properties(SampleModalityWorklists PROPERTIES + VERSION ${SAMPLE_MODALITY_WORKLISTS_VERSION} + SOVERSION ${SAMPLE_MODALITY_WORKLISTS_VERSION}) + +install( + TARGETS SampleModalityWorklists + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/Plugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -0,0 +1,234 @@ +/** + * 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 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 + * 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 . + **/ + + +#include + +#include +#include +#include +#include +#include + +static OrthancPluginContext* context_ = NULL; +static std::string folder_; + + +static bool ReadFile(std::string& result, + const std::string& path) +{ + OrthancPluginMemoryBuffer tmp; + if (OrthancPluginReadFile(context_, &tmp, path.c_str())) + { + return false; + } + else + { + result.assign(reinterpret_cast(tmp.data), tmp.size); + OrthancPluginFreeMemoryBuffer(context_, &tmp); + return true; + } +} + + +/** + * This is the main function for matching a DICOM worklist against a query. + **/ +static OrthancPluginErrorCode MatchWorklist(OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const std::string& path) +{ + std::string dicom; + if (!ReadFile(dicom, path)) + { + // Cannot read this file, ignore this error + return OrthancPluginErrorCode_Success; + } + + if (OrthancPluginIsWorklistMatch(context_, query, dicom.c_str(), dicom.size())) + { + // This DICOM file matches the worklist query, add it to the answers + return OrthancPluginWorklistAddWorklistAnswer + (context_, answers, query, dicom.c_str(), dicom.size()); + } + else + { + // This DICOM file does not match + return OrthancPluginErrorCode_Success; + } +} + + + +static bool ConvertToJson(Json::Value& result, + char* content) +{ + if (content == NULL) + { + return false; + } + else + { + Json::Reader reader; + bool success = reader.parse(content, content + strlen(content), result); + OrthancPluginFreeString(context_, content); + return success; + } +} + + + +static bool GetQueryDicom(Json::Value& value, + const OrthancPluginWorklistQuery* query) +{ + OrthancPluginMemoryBuffer dicom; + if (OrthancPluginGetWorklistQueryDicom(context_, &dicom, query)) + { + return false; + } + + char* json = OrthancPluginDicomBufferToJson(context_, reinterpret_cast(dicom.data), + dicom.size, + OrthancPluginDicomToJsonFormat_Short, + static_cast(0), 0); + OrthancPluginFreeMemoryBuffer(context_, &dicom); + + return ConvertToJson(value, json); +} + + +OrthancPluginErrorCode Callback(OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const char* remoteAet, + const char* calledAet) +{ + Json::Value json; + + if (!GetQueryDicom(json, query)) + { + return OrthancPluginErrorCode_InternalError; + } + + std::cout << "Received worklist query from remote modality " << remoteAet + << ":" << std::endl << json.toStyledString(); + + boost::filesystem::path source(folder_); + boost::filesystem::directory_iterator end; + + try + { + for (boost::filesystem::directory_iterator it(source); it != end; ++it) + { + if (is_regular_file(it->status())) + { + std::string extension = boost::filesystem::extension(it->path()); + if (!strcasecmp(".wl", extension.c_str())) + { + OrthancPluginErrorCode error = MatchWorklist(answers, query, it->path().string()); + if (error) + { + OrthancPluginLogError(context_, "Error while adding an answer to a worklist request"); + return error; + } + } + } + } + } + catch (boost::filesystem::filesystem_error&) + { + std::string description = std::string("Inexistent folder while scanning for worklists: ") + source.string(); + OrthancPluginLogError(context_, description.c_str()); + return OrthancPluginErrorCode_DirectoryExpected; + } + + // Uncomment the following line if too many answers are to be returned + // OrthancPluginMarkWorklistAnswersIncomplete(context_, answers); + + return OrthancPluginErrorCode_Success; +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + context_ = c; + OrthancPluginLogWarning(context_, "Storage plugin is initializing"); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 0) + { + char info[1024]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + context_->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(context_, info); + return -1; + } + + Json::Value configuration; + if (!ConvertToJson(configuration, OrthancPluginGetConfiguration(context_))) + { + OrthancPluginLogError(context_, "Cannot access the configuration"); + return -1; + } + + if (configuration.isMember("WorklistsFolder")) + { + if (configuration["WorklistsFolder"].type() != Json::stringValue) + { + OrthancPluginLogError(context_, "The configuration option \"WorklistsFolder\" must be a string"); + return -1; + } + + folder_ = configuration["WorklistsFolder"].asString(); + } + else + { + folder_ = DEFAULT_WORKLISTS_FOLDER; + } + + std::string message = "The database of worklists will be read from folder: " + folder_; + OrthancPluginLogWarning(context_, message.c_str()); + + OrthancPluginRegisterWorklistCallback(context_, Callback); + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + OrthancPluginLogWarning(context_, "Sample worklist plugin is finalizing"); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "sample-worklists"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return SAMPLE_MODALITY_WORKLISTS_VERSION; + } +} diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/Generate.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ModalityWorklists/WorklistsDatabase/Generate.py Fri Nov 20 16:50:01 2015 +0100 @@ -0,0 +1,19 @@ +#!/usr/bin/python + +import os +import subprocess + +SOURCE = '/home/jodogne/Downloads/dcmtk-3.6.0/dcmwlm/data/wlistdb/OFFIS/' +TARGET = os.path.abspath(os.path.dirname(__file__)) + +for f in os.listdir(SOURCE): + ext = os.path.splitext(f) + + if ext[1].lower() == '.dump': + subprocess.check_call([ + 'dump2dcm', + '-g', + '-q', + os.path.join(SOURCE, f), + os.path.join(TARGET, ext[0].lower() + '.wl'), + ]) diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist1.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist1.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist10.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist10.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist2.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist2.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist3.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist3.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist4.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist4.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist5.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist5.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist6.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist6.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist7.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist7.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist8.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist8.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist9.wl Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist9.wl has changed diff -r 9a3a77d1d2a2 -r bdef349f742c Resources/DicomConformanceStatement.txt --- a/Resources/DicomConformanceStatement.txt Thu Nov 19 11:57:32 2015 +0100 +++ b/Resources/DicomConformanceStatement.txt Fri Nov 20 16:50:01 2015 +0100 @@ -149,6 +149,7 @@ FINDPatientRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.1.1 FINDStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.1 + FINDModalityWorklistInformationModel | 1.2.840.10008.5.1.4.31 -------------------- diff -r 9a3a77d1d2a2 -r bdef349f742c Resources/ErrorCodes.json --- a/Resources/ErrorCodes.json Thu Nov 19 11:57:32 2015 +0100 +++ b/Resources/ErrorCodes.json Fri Nov 20 16:50:01 2015 +0100 @@ -513,5 +513,10 @@ "Code": 2040, "Name": "CannotOrderSlices", "Description": "Unable to order the slices of the series" + }, + { + "Code": 2041, + "Name": "NoWorklistHandler", + "Description": "No request handler factory for DICOM C-Find Modality SCP" } ] diff -r 9a3a77d1d2a2 -r bdef349f742c UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/UnitTestsSources/FromDcmtkTests.cpp Fri Nov 20 16:50:01 2015 +0100 @@ -43,6 +43,7 @@ #include "../Core/Images/PngWriter.h" #include "../Core/Uuid.h" #include "../Resources/EncodingTests.h" +#include "../OrthancServer/DicomProtocol/DicomFindAnswers.h" #include @@ -606,3 +607,33 @@ ASSERT_EQ("application/octet-stream", mime); ASSERT_EQ("Pixels", content); } + + +TEST(DicomFindAnswers, Basic) +{ + DicomFindAnswers a; + + { + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_ID, "hello"); + a.Add(m); + } + + { + ParsedDicomFile d; + d.Replace(DICOM_TAG_PATIENT_ID, "my"); + a.Add(d); + } + + { + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_ID, "world"); + a.Add(m); + } + + Json::Value j; + a.ToJson(j, true); + ASSERT_EQ(3u, j.size()); + + //std::cout << j; +}