# HG changeset patch # User Sebastien Jodogne # Date 1579194883 -3600 # Node ID e327b44780bb874f3bce45be7bc3d1c17ae06d7b # Parent 7e303ba837d9a4f613b854206714c1c35778d79b abstraction: storage commitment handler diff -r 7e303ba837d9 -r e327b44780bb Core/DicomNetworking/DicomServer.cpp --- a/Core/DicomNetworking/DicomServer.cpp Thu Jan 16 18:10:57 2020 +0100 +++ b/Core/DicomNetworking/DicomServer.cpp Thu Jan 16 18:14:43 2020 +0100 @@ -94,6 +94,7 @@ moveRequestHandlerFactory_ = NULL; storeRequestHandlerFactory_ = NULL; worklistRequestHandlerFactory_ = NULL; + storageCommitmentFactory_ = NULL; applicationEntityFilter_ = NULL; checkCalledAet_ = true; associationTimeout_ = 30; @@ -289,6 +290,29 @@ } } + void DicomServer::SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& factory) + { + Stop(); + storageCommitmentFactory_ = &factory; + } + + bool DicomServer::HasStorageCommitmentRequestHandlerFactory() const + { + return (storageCommitmentFactory_ != NULL); + } + + IStorageCommitmentRequestHandlerFactory& DicomServer::GetStorageCommitmentRequestHandlerFactory() const + { + if (HasStorageCommitmentRequestHandlerFactory()) + { + return *storageCommitmentFactory_; + } + else + { + throw OrthancException(ErrorCode_NoStorageCommitmentHandler); + } + } + void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory) { Stop(); @@ -378,5 +402,4 @@ return modalities_->IsSameAETitle(aet, GetApplicationEntityTitle()); } } - } diff -r 7e303ba837d9 -r e327b44780bb Core/DicomNetworking/DicomServer.h --- a/Core/DicomNetworking/DicomServer.h Thu Jan 16 18:10:57 2020 +0100 +++ b/Core/DicomNetworking/DicomServer.h Thu Jan 16 18:14:43 2020 +0100 @@ -41,6 +41,7 @@ #include "IMoveRequestHandlerFactory.h" #include "IStoreRequestHandlerFactory.h" #include "IWorklistRequestHandlerFactory.h" +#include "IStorageCommitmentRequestHandlerFactory.h" #include "IApplicationEntityFilter.h" #include "RemoteModalityParameters.h" @@ -82,6 +83,7 @@ IMoveRequestHandlerFactory* moveRequestHandlerFactory_; IStoreRequestHandlerFactory* storeRequestHandlerFactory_; IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_; + IStorageCommitmentRequestHandlerFactory* storageCommitmentFactory_; IApplicationEntityFilter* applicationEntityFilter_; static void ServerThread(DicomServer* server); @@ -122,6 +124,10 @@ bool HasWorklistRequestHandlerFactory() const; IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const; + void SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& handler); + bool HasStorageCommitmentRequestHandlerFactory() const; + IStorageCommitmentRequestHandlerFactory& GetStorageCommitmentRequestHandlerFactory() const; + void SetApplicationEntityFilter(IApplicationEntityFilter& handler); bool HasApplicationEntityFilter() const; IApplicationEntityFilter& GetApplicationEntityFilter() const; diff -r 7e303ba837d9 -r e327b44780bb Core/DicomNetworking/IStorageCommitmentRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IStorageCommitmentRequestHandler.h Thu Jan 16 18:14:43 2020 +0100 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "DicomFindAnswers.h" + +namespace Orthanc +{ + class IStorageCommitmentRequestHandler : public boost::noncopyable + { + public: + virtual ~IStorageCommitmentRequestHandler() + { + } + + virtual void Handle(const std::string& transactionUid, + const std::vector& referencedSopClassUids, + const std::vector& referencedSopInstanceUids, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; + }; +} diff -r 7e303ba837d9 -r e327b44780bb Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h Thu Jan 16 18:14:43 2020 +0100 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IStorageCommitmentRequestHandler.h" + +namespace Orthanc +{ + class IStorageCommitmentRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IStorageCommitmentRequestHandlerFactory() + { + } + + virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() = 0; + }; +} diff -r 7e303ba837d9 -r e327b44780bb Core/DicomNetworking/Internals/CommandDispatcher.cpp --- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp Thu Jan 16 18:10:57 2020 +0100 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Thu Jan 16 18:14:43 2020 +0100 @@ -92,9 +92,12 @@ #include "MoveScp.h" #include "../../Toolbox.h" #include "../../Logging.h" +#include "../../OrthancException.h" +#include /* for storage commitment */ +#include /* for class DcmSequenceOfItems */ +#include /* for variable dcmAllStorageSOPClassUIDs */ #include /* for class DcmAssociationConfiguration */ -#include /* for variable dcmAllStorageSOPClassUIDs */ #include @@ -298,6 +301,12 @@ knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel); } + // For Storage Commitment + if (server.HasStorageCommitmentRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_StorageCommitmentPushModelSOPClass); + } + cond = ASC_receiveAssociation(net, &assoc, /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, NULL, NULL, @@ -689,6 +698,11 @@ supported = true; break; + case DIMSE_N_ACTION_RQ: + request = DicomRequestType_NAction; + supported = true; + break; + default: // we cannot handle this kind of message cond = DIMSE_BADCOMMANDTYPE; @@ -770,6 +784,10 @@ } break; + case DicomRequestType_NAction: + cond = NActionScp(&msg, presID); + break; + default: // Should never happen break; @@ -823,5 +841,187 @@ } return cond; } + + + OFCondition CommandDispatcher::NActionScp(T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID) + { + /** + * Starting with Orthanc 1.6.0, only storage commitment is + * supported with DICOM N-ACTION. This corresponds to the case + * where "Action Type ID" equals "1". + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4 + **/ + + if (msg->CommandField != DIMSE_N_ACTION_RQ /* value == 304 == 0x0130 */ || + !server_.HasStorageCommitmentRequestHandlerFactory()) + { + throw OrthancException(ErrorCode_InternalError); + } + + + /** + * Check that the storage commitment request is correctly formatted. + **/ + + const T_DIMSE_N_ActionRQ& request = msg->msg.NActionRQ; + + if (request.ActionTypeID != 1) + { + throw OrthancException(ErrorCode_NotImplemented, + "Only storage commitment is implemented for DICOM N-ACTION SCP"); + } + + if (std::string(request.RequestedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || + std::string(request.RequestedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Unexpected incoming SOP class or instance UID for storage commitment"); + } + + if (request.DataSetType != DIMSE_DATASET_PRESENT) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Incoming storage commitment request without a dataset"); + } + + + /** + * Extract the DICOM dataset that is associated with the DIMSE + * message. The content of this dataset is documented in "Table + * J.3-1. Storage Commitment Request - Action Information": + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html#table_J.3-1 + **/ + + std::auto_ptr dataset; + + { + DcmDataset *tmp = NULL; + T_ASC_PresentationContextID presIdData; + + OFCondition cond = DIMSE_receiveDataSetInMemory( + assoc_, /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ 0, &presIdData, &tmp, NULL, NULL); + if (!cond.good()) + { + return cond; + } + + if (tmp == NULL) + { + LOG(ERROR) << "Cannot read the dataset in N-ACTION SCP"; + return EC_InvalidStream; + } + + dataset.reset(tmp); + } + + std::string transactionUid; + std::vector referencedSopClassUid, referencedSopInstanceUid; + + { + const char* s = NULL; + if (!dataset->findAndGetString(DCM_TransactionUID, s).good() || + s == NULL) + { + LOG(ERROR) << "Missing Transaction UID in storage commitment request"; + return EC_InvalidStream; + } + + transactionUid.assign(s); + } + + { + DcmSequenceOfItems* sequence = NULL; + if (!dataset->findAndGetSequence(DCM_ReferencedSOPSequence, sequence).good() || + sequence == NULL) + { + LOG(ERROR) << "Missing Referenced SOP Sequence in storage commitment request"; + return EC_InvalidStream; + } + + referencedSopClassUid.reserve(sequence->card()); + referencedSopInstanceUid.reserve(sequence->card()); + + for (unsigned long i = 0; i < sequence->card(); i++) + { + const char* a = NULL; + const char* b = NULL; + if (!sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPClassUID, a).good() || + !sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPInstanceUID, b).good() || + a == NULL || + b == NULL) + { + LOG(ERROR) << "Missing Referenced SOP Class/Instance UID in storage commitment request"; + return EC_InvalidStream; + } + + referencedSopClassUid.push_back(a); + referencedSopInstanceUid.push_back(b); + } + } + + LOG(INFO) << "Incoming storage commitment transaction, with UID: " << transactionUid; + + for (size_t i = 0; i < referencedSopClassUid.size(); i++) + { + LOG(INFO) << " (" << (i + 1) << "/" << referencedSopClassUid.size() + << ") queried SOP Class/Instance UID: " + << referencedSopClassUid[i] << " / " << referencedSopInstanceUid[i]; + } + + + /** + * Call the Orthanc handler. The list of available DIMSE status + * codes can be found at: + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10 + **/ + + DIC_US dimseStatus; + + try + { + std::auto_ptr handler + (server_.GetStorageCommitmentRequestHandlerFactory(). + ConstructStorageCommitmentRequestHandler()); + + handler->Handle(transactionUid, referencedSopClassUid, referencedSopInstanceUid, + remoteIp_, remoteAet_, calledAet_); + + dimseStatus = 0; // Success + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error while processing an incoming storage commitment request: " << e.What(); + + // Code 0x0110 - "General failure in processing the operation was encountered" + dimseStatus = STATUS_N_ProcessingFailure; + } + + + /** + * Send the DIMSE status back to the SCU. + **/ + + { + T_DIMSE_Message response; + memset(&response, 0, sizeof(response)); + response.CommandField = DIMSE_N_ACTION_RSP; + + T_DIMSE_N_ActionRSP& content = response.msg.NActionRSP; + content.MessageIDBeingRespondedTo = request.MessageID; + strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); + content.DimseStatus = dimseStatus; + strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); + content.ActionTypeID = 0; // Not present, as "O_NACTION_ACTIONTYPEID" not set in "opts" + content.DataSetType = DIMSE_DATASET_NULL; // Dataset is absent in storage commitment response + content.opts = O_NACTION_AFFECTEDSOPCLASSUID | O_NACTION_AFFECTEDSOPINSTANCEUID; + + return DIMSE_sendMessageUsingMemoryData( + assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */, + NULL /* callback */, NULL /* callback context */, NULL /* commandSet */); + } + } } } diff -r 7e303ba837d9 -r e327b44780bb Core/DicomNetworking/Internals/CommandDispatcher.h --- a/Core/DicomNetworking/Internals/CommandDispatcher.h Thu Jan 16 18:10:57 2020 +0100 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.h Thu Jan 16 18:14:43 2020 +0100 @@ -56,6 +56,9 @@ std::string calledAet_; IApplicationEntityFilter* filter_; + OFCondition NActionScp(T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID); + public: CommandDispatcher(const DicomServer& server, T_ASC_Association* assoc, @@ -69,11 +72,11 @@ virtual bool Step(); }; - OFCondition EchoScp(T_ASC_Association * assoc, - T_DIMSE_Message * msg, - T_ASC_PresentationContextID presID); - CommandDispatcher* AcceptAssociation(const DicomServer& server, T_ASC_Network *net); + + OFCondition EchoScp(T_ASC_Association* assoc, + T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID); } } diff -r 7e303ba837d9 -r e327b44780bb OrthancServer/main.cpp --- a/OrthancServer/main.cpp Thu Jan 16 18:10:57 2020 +0100 +++ b/OrthancServer/main.cpp Thu Jan 16 18:14:43 2020 +0100 @@ -91,6 +91,30 @@ +class OrthancStorageCommitmentRequestHandler : public IStorageCommitmentRequestHandler +{ +private: + ServerContext& server_; + +public: + OrthancStorageCommitmentRequestHandler(ServerContext& context) : + server_(context) + { + } + + virtual void Handle(const std::string& transactionUid, + const std::vector& referencedSopClassUids, + const std::vector& referencedSopInstanceUids, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + // TODO - Enqueue a Storage commitment job + } +}; + + + class ModalitiesFromConfiguration : public DicomServer::IRemoteModalities { public: @@ -113,7 +137,8 @@ class MyDicomServerFactory : public IStoreRequestHandlerFactory, public IFindRequestHandlerFactory, - public IMoveRequestHandlerFactory + public IMoveRequestHandlerFactory, + public IStorageCommitmentRequestHandlerFactory { private: ServerContext& context_; @@ -166,6 +191,11 @@ return new OrthancMoveRequestHandler(context_); } + virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() + { + return new OrthancStorageCommitmentRequestHandler(context_); + } + void Done() { } @@ -672,6 +702,7 @@ PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series"); PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP"); PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists"); + PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)"); PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type"); } @@ -966,6 +997,7 @@ dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory); dicomServer.SetFindRequestHandlerFactory(serverFactory); + dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory); { OrthancConfiguration::ReaderLock lock;