Mercurial > hg > orthanc
changeset 1794:bdfae6e17d23 worklists
integration mainline->worklists
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 19 Nov 2015 16:02:35 +0100 |
parents | b769623c806c (diff) 9a3a77d1d2a2 (current diff) |
children | af6840eb23ee |
files | OrthancServer/DicomInstanceToStore.cpp OrthancServer/ServerEnumerations.h |
diffstat | 37 files changed, 672 insertions(+), 169 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Thu Nov 19 11:57:32 2015 +0100 +++ b/CMakeLists.txt Thu Nov 19 16:02:35 2015 +0100 @@ -179,6 +179,7 @@ OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/ParsedDicomFile.cpp OrthancServer/QueryRetrieveHandler.cpp + OrthancServer/Search/IFindConstraint.cpp OrthancServer/Search/LookupIdentifierQuery.cpp OrthancServer/Search/LookupResource.cpp OrthancServer/Search/SetOfResources.cpp
--- a/Core/Enumerations.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/Core/Enumerations.cpp Thu Nov 19 16:02:35 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) {
--- a/Core/Enumerations.h Thu Nov 19 11:57:32 2015 +0100 +++ b/Core/Enumerations.h Thu Nov 19 16:02:35 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 };
--- a/NEWS Thu Nov 19 11:57:32 2015 +0100 +++ b/NEWS Thu Nov 19 16:02:35 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
--- a/OrthancServer/DicomDirWriter.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomDirWriter.cpp Thu Nov 19 16:02:35 2015 +0100 @@ -527,7 +527,7 @@ path = directory + '\\' + filename; } - DcmFileFormat& fileFormat = *reinterpret_cast<DcmFileFormat*>(dicom.GetDcmtkObject()); + DcmFileFormat& fileFormat = dicom.GetDcmtkObject(); DcmDirectoryRecord* instance; bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str());
--- a/OrthancServer/DicomInstanceToStore.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomInstanceToStore.cpp Thu Nov 19 16:02:35 2015 +0100 @@ -41,12 +41,6 @@ namespace Orthanc { - static DcmDataset& GetDataset(ParsedDicomFile& file) - { - return *reinterpret_cast<DcmFileFormat*>(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 */);
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Thu Nov 19 16:02:35 2015 +0100 @@ -34,25 +34,154 @@ #include "DicomFindAnswers.h" #include "../FromDcmtkBridge.h" +#include "../ToDcmtkBridge.h" +#include "../../Core/OrthancException.h" + +#include <memory> +#include <dcmtk/dcmdata/dcfilefo.h> + namespace Orthanc { + class DicomFindAnswers::Answer + { + private: + ParsedDicomFile* dicom_; + DicomMap* map_; + + public: + Answer(ParsedDicomFile& dicom) : + dicom_(dicom.Clone()), + map_(NULL) + { + } + + Answer(const char* dicom, + size_t size) : + dicom_(new ParsedDicomFile(dicom, size)), + map_(NULL) + { + } + + 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 +189,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); } }
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h Thu Nov 19 16:02:35 2015 +0100 @@ -32,17 +32,18 @@ #pragma once -#include "../../Core/DicomFormat/DicomMap.h" - -#include <vector> -#include <json/json.h> +#include "../ParsedDicomFile.h" namespace Orthanc { - class DicomFindAnswers + class DicomFindAnswers : public boost::noncopyable { private: - std::vector<DicomMap*> items_; + class Answer; + + std::vector<Answer*> answers_; + + Answer& GetAnswerInternal(size_t index) const; public: ~DicomFindAnswers() @@ -54,22 +55,27 @@ 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; }; }
--- a/OrthancServer/DicomProtocol/DicomServer.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomServer.cpp Thu Nov 19 16:02:35 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();
--- a/OrthancServer/DicomProtocol/DicomServer.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomServer.h Thu Nov 19 16:02:35 2015 +0100 @@ -35,6 +35,7 @@ #include "IFindRequestHandlerFactory.h" #include "IMoveRequestHandlerFactory.h" #include "IStoreRequestHandlerFactory.h" +#include "IWorklistRequestHandlerFactory.h" #include "IApplicationEntityFilter.h" #include <boost/shared_ptr.hpp> @@ -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;
--- a/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Thu Nov 19 16:02:35 2015 +0100 @@ -38,7 +38,7 @@ namespace Orthanc { - class IApplicationEntityFilter + class IApplicationEntityFilter : public boost::noncopyable { public: virtual ~IApplicationEntityFilter()
--- a/OrthancServer/DicomProtocol/IFindRequestHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IFindRequestHandler.h Thu Nov 19 16:02:35 2015 +0100 @@ -34,13 +34,9 @@ #include "DicomFindAnswers.h" -#include <vector> -#include <string> - - namespace Orthanc { - class IFindRequestHandler + class IFindRequestHandler : public boost::noncopyable { public: virtual ~IFindRequestHandler()
--- a/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Thu Nov 19 16:02:35 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class IFindRequestHandlerFactory + class IFindRequestHandlerFactory : public boost::noncopyable { public: virtual ~IFindRequestHandlerFactory()
--- a/OrthancServer/DicomProtocol/IMoveRequestHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandler.h Thu Nov 19 16:02:35 2015 +0100 @@ -40,7 +40,7 @@ namespace Orthanc { - class IMoveRequestIterator + class IMoveRequestIterator : public boost::noncopyable { public: enum Status
--- a/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Thu Nov 19 16:02:35 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class IMoveRequestHandlerFactory + class IMoveRequestHandlerFactory : public boost::noncopyable { public: virtual ~IMoveRequestHandlerFactory()
--- a/OrthancServer/DicomProtocol/IStoreRequestHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandler.h Thu Nov 19 16:02:35 2015 +0100 @@ -40,7 +40,7 @@ namespace Orthanc { - class IStoreRequestHandler + class IStoreRequestHandler : public boost::noncopyable { public: virtual ~IStoreRequestHandler()
--- a/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Thu Nov 19 16:02:35 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class IStoreRequestHandlerFactory + class IStoreRequestHandlerFactory : public boost::noncopyable { public: virtual ~IStoreRequestHandlerFactory()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomProtocol/IWorklistRequestHandler.h Thu Nov 19 16:02:35 2015 +0100 @@ -0,0 +1,57 @@ +/** + * 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "DicomFindAnswers.h" + +namespace Orthanc +{ + class IWorklistRequestHandler : public boost::noncopyable + { + public: + virtual ~IWorklistRequestHandler() + { + } + + /** + * 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, + const ParsedDicomFile& query, + const std::string& remoteIp, + const std::string& remoteAet) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomProtocol/IWorklistRequestHandlerFactory.h Thu Nov 19 16:02:35 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "IWorklistRequestHandler.h" + +namespace Orthanc +{ + class IWorklistRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IWorklistRequestHandlerFactory() + { + } + + virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() = 0; + }; +}
--- a/OrthancServer/FromDcmtkBridge.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/FromDcmtkBridge.cpp Thu Nov 19 16:02:35 2015 +0100 @@ -1073,6 +1073,9 @@ case EVR_TM: return ValueRepresentation_Time; + case EVR_SQ: + return ValueRepresentation_Sequence; + default: return ValueRepresentation_Other; }
--- a/OrthancServer/Internals/CommandDispatcher.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/CommandDispatcher.cpp Thu Nov 19 16:02:35 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()) { @@ -812,11 +817,23 @@ break; case DicomRequestType_Find: - if (server_.HasFindRequestHandlerFactory()) // Should always be true + if (server_.HasFindRequestHandlerFactory() || // Should always be true + server_.HasWorklistRequestHandlerFactory()) { - std::auto_ptr<IFindRequestHandler> handler - (server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler()); - cond = Internals::findScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_); + std::auto_ptr<IFindRequestHandler> findHandler; + if (server_.HasFindRequestHandlerFactory()) + { + findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler()); + } + + std::auto_ptr<IWorklistRequestHandler> worklistHandler; + if (server_.HasWorklistRequestHandlerFactory()) + { + worklistHandler.reset(server_.GetWorklistRequestHandlerFactory().ConstructWorklistRequestHandler()); + } + + cond = Internals::findScp(assoc_, &msg, presID, findHandler.get(), + worklistHandler.get(), remoteIp_, remoteAet_); } break;
--- a/OrthancServer/Internals/FindScp.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/FindScp.cpp Thu Nov 19 16:02:35 2015 +0100 @@ -87,7 +87,7 @@ #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" - +#include <dcmtk/dcmdata/dcfilefo.h> namespace Orthanc { @@ -95,8 +95,8 @@ { struct FindScpData { - IFindRequestHandler* handler_; - DicomMap input_; + IFindRequestHandler* findHandler_; + IWorklistRequestHandler* worklistHandler_; DicomFindAnswers answers_; DcmDataset* lastRequest_; const std::string* remoteIp_; @@ -120,20 +120,53 @@ bzero(response, sizeof(T_DIMSE_C_FindRSP)); *statusDetail = NULL; + std::string sopClassUid(request->AffectedSOPClassUID); + FindScpData& data = *reinterpret_cast<FindScpData*>(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.noCroppingOfResults_ = data.worklistHandler_->Handle(data.answers_, query, + *data.remoteIp_, *data.remoteAet_); + 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.noCroppingOfResults_ = data.findHandler_->Handle(data.answers_, input, + *data.remoteIp_, *data.remoteAet_); + 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,7 +186,7 @@ { // 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_) { @@ -175,13 +208,15 @@ 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) { FindScpData data; data.lastRequest_ = NULL; - data.handler_ = &handler; + data.findHandler_ = findHandler; + data.worklistHandler_ = worklistHandler; data.remoteIp_ = &remoteIp; data.remoteAet_ = &remoteAet; data.noCroppingOfResults_ = true;
--- a/OrthancServer/Internals/FindScp.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/FindScp.h Thu Nov 19 16:02:35 2015 +0100 @@ -33,6 +33,7 @@ #pragma once #include "../DicomProtocol/IFindRequestHandler.h" +#include "../DicomProtocol/IWorklistRequestHandler.h" #include <dcmtk/dcmnet/dimse.h> @@ -43,7 +44,8 @@ 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); }
--- a/OrthancServer/Internals/MoveScp.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Internals/MoveScp.cpp Thu Nov 19 16:02:35 2015 +0100 @@ -98,7 +98,6 @@ { std::string target_; IMoveRequestHandler* handler_; - DicomMap input_; DcmDataset* lastRequest_; unsigned int subOperationCount_; unsigned int failureCount_; @@ -128,11 +127,12 @@ MoveScpData& data = *reinterpret_cast<MoveScpData*>(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.iterator_.reset(data.handler_->Handle(data.target_, input, *data.remoteIp_, *data.remoteAet_)); if (data.iterator_.get() == NULL)
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Nov 19 16:02:35 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<size_t>(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<size_t>(call.GetUriComponent("index", "")); - query->GetAnswer(index); + + DicomMap map; + query->GetAnswer(map, index); RestApi::AutoListChildren(call); }
--- a/OrthancServer/ParsedDicomFile.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/ParsedDicomFile.cpp Thu Nov 19 16:02:35 2015 +0100 @@ -823,6 +823,18 @@ } + ParsedDicomFile::ParsedDicomFile(const DicomMap& map) : pimpl_(new PImpl) + { + std::auto_ptr<DcmDataset> 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<DcmFileFormat> fileFormat(new DcmFileFormat(dataset.get())); + + pimpl_->file_.reset(fileFormat.release()); + } + + ParsedDicomFile::ParsedDicomFile(const char* content, size_t size) : pimpl_(new PImpl) { Setup(content, size); @@ -851,15 +863,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(); }
--- a/OrthancServer/ParsedDicomFile.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/ParsedDicomFile.h Thu Nov 19 16:02:35 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 @@ -61,14 +64,20 @@ public: ParsedDicomFile(); // Create a minimal DICOM instance + ParsedDicomFile(const DicomMap& map); + ParsedDicomFile(const char* content, size_t size); ParsedDicomFile(const std::string& content); + ParsedDicomFile(DcmDataset& dicom); + + ParsedDicomFile(DcmFileFormat& dicom); + ~ParsedDicomFile(); - void* GetDcmtkObject(); + DcmFileFormat& GetDcmtkObject(); ParsedDicomFile* Clone();
--- a/OrthancServer/QueryRetrieveHandler.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/QueryRetrieveHandler.cpp Thu Nov 19 16:02:35 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)); }
--- a/OrthancServer/QueryRetrieveHandler.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/QueryRetrieveHandler.h Thu Nov 19 16:02:35 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);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/IFindConstraint.cpp Thu Nov 19 16:02:35 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 <http://www.gnu.org/licenses/>. + **/ + + +#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<ListConstraint> constraint(new ListConstraint(caseSensitive)); + + std::vector<std::string> 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); + } + } +}
--- a/OrthancServer/Search/IFindConstraint.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/IFindConstraint.h Thu Nov 19 16:02:35 2015 +0100 @@ -49,5 +49,9 @@ const DicomTag& tag) const = 0; virtual bool Match(const std::string& value) const = 0; + + static IFindConstraint* ParseDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive); }; }
--- a/OrthancServer/Search/LookupResource.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/Search/LookupResource.cpp Thu Nov 19 16:02:35 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<ListConstraint> constraint(new ListConstraint(caseSensitive)); - - std::vector<std::string> 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)); } } + }
--- a/OrthancServer/ServerEnumerations.h Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/ServerEnumerations.h Thu Nov 19 16:02:35 2015 +0100 @@ -98,7 +98,8 @@ ValueRepresentation_PatientName, ValueRepresentation_Date, ValueRepresentation_DateTime, - ValueRepresentation_Time + ValueRepresentation_Time, + ValueRepresentation_Sequence }; enum DicomToJsonFormat
--- a/OrthancServer/main.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/OrthancServer/main.cpp Thu Nov 19 16:02:35 2015 +0100 @@ -89,10 +89,32 @@ }; +class OrthancWorklistRequestHandler : public IWorklistRequestHandler +{ +private: + ServerContext& server_; + +public: + OrthancWorklistRequestHandler(ServerContext& context) : + server_(context) + { + } + + virtual bool Handle(DicomFindAnswers& answers, + const ParsedDicomFile& query, + const std::string& remoteIp, + const std::string& remoteAet) + { + printf("Worklist\n"); + return true; + } +}; + class MyDicomServerFactory : public IStoreRequestHandlerFactory, - public IFindRequestHandlerFactory, + public IFindRequestHandlerFactory, + public IWorklistRequestHandlerFactory, public IMoveRequestHandlerFactory { private: @@ -143,6 +165,11 @@ return new OrthancMoveRequestHandler(context_); } + virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() + { + return new OrthancWorklistRequestHandler(context_); + } + void Done() { } @@ -550,6 +577,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 +732,10 @@ dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory); dicomServer.SetFindRequestHandlerFactory(serverFactory); + + // TODO - Disable the following line if no worklist plugin is available + dicomServer.SetWorklistRequestHandlerFactory(serverFactory); + dicomServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("DicomPort", 4242)); dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")); dicomServer.SetApplicationEntityFilter(dicomFilter);
--- a/Plugins/Include/orthanc/OrthancCPlugin.h Thu Nov 19 11:57:32 2015 +0100 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Thu Nov 19 16:02:35 2015 +0100 @@ -271,6 +271,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;
--- a/Resources/ErrorCodes.json Thu Nov 19 11:57:32 2015 +0100 +++ b/Resources/ErrorCodes.json Thu Nov 19 16:02:35 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" } ]
--- a/UnitTestsSources/FromDcmtkTests.cpp Thu Nov 19 11:57:32 2015 +0100 +++ b/UnitTestsSources/FromDcmtkTests.cpp Thu Nov 19 16:02:35 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 <dcmtk/dcmdata/dcelem.h> @@ -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; +}