Mercurial > hg > orthanc
changeset 3644:e168a2dedb00 storage-commitment
integration mainline->storage-commitment
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 04 Feb 2020 08:22:22 +0100 |
parents | fddf3fc82362 (diff) fa3ff492fb3b (current diff) |
children | 5faf76511931 |
files | Core/Enumerations.cpp Core/Enumerations.h OrthancServer/main.cpp Resources/Configuration.json |
diffstat | 29 files changed, 1913 insertions(+), 195 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Tue Feb 04 08:22:02 2020 +0100 +++ b/CMakeLists.txt Tue Feb 04 08:22:22 2020 +0100 @@ -103,6 +103,7 @@ OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp OrthancServer/ServerJobs/ResourceModificationJob.cpp OrthancServer/ServerJobs/SplitStudyJob.cpp + OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp OrthancServer/ServerToolbox.cpp OrthancServer/SliceOrdering.cpp ) @@ -483,9 +484,13 @@ ) endif() - externalproject_add(ConnectivityChecksProject + externalproject_add(ConnectivityChecks SOURCE_DIR "${ORTHANC_ROOT}/Plugins/Samples/ConnectivityChecks" + # We explicitly provide a build directory, in order to avoid paths + # that are too long on our Visual Studio 2008 CIS + BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/ConnectivityChecks-build" + CMAKE_ARGS -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
--- a/Core/DicomNetworking/DicomServer.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/DicomNetworking/DicomServer.cpp Tue Feb 04 08:22:22 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()); } } - }
--- a/Core/DicomNetworking/DicomServer.h Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/DicomNetworking/DicomServer.h Tue Feb 04 08:22:22 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;
--- a/Core/DicomNetworking/DicomUserConnection.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -252,7 +252,8 @@ } - void DicomUserConnection::SetupPresentationContexts(const std::string& preferredTransferSyntax) + void DicomUserConnection::SetupPresentationContexts(Mode mode, + const std::string& preferredTransferSyntax) { // Flatten an array with the preferred transfer syntax const char* asPreferred[1] = { preferredTransferSyntax.c_str() }; @@ -274,30 +275,73 @@ } CheckStorageSOPClassesInvariant(); - unsigned int presentationContextId = 1; + + switch (mode) + { + case Mode_Generic: + { + unsigned int presentationContextId = 1; + + for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin(); + it != reservedStorageSOPClasses_.end(); ++it) + { + RegisterStorageSOPClass(pimpl_->params_, presentationContextId, + *it, asPreferred, asFallback, remoteAet_); + } - for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin(); - it != reservedStorageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); - } + for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin(); + it != storageSOPClasses_.end(); ++it) + { + RegisterStorageSOPClass(pimpl_->params_, presentationContextId, + *it, asPreferred, asFallback, remoteAet_); + } + + for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin(); + it != defaultStorageSOPClasses_.end(); ++it) + { + RegisterStorageSOPClass(pimpl_->params_, presentationContextId, + *it, asPreferred, asFallback, remoteAet_); + } + + break; + } - for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin(); - it != storageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); - } + case Mode_RequestStorageCommitment: + case Mode_ReportStorageCommitment: + { + const char* as = UID_StorageCommitmentPushModelSOPClass; + + std::vector<const char*> ts; + ts.push_back(UID_LittleEndianExplicitTransferSyntax); + ts.push_back(UID_LittleEndianImplicitTransferSyntax); - for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin(); - it != defaultStorageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); + T_ASC_SC_ROLE role; + switch (mode) + { + case Mode_RequestStorageCommitment: + role = ASC_SC_ROLE_DEFAULT; + break; + + case Mode_ReportStorageCommitment: + role = ASC_SC_ROLE_SCP; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + Check(ASC_addPresentationContext(pimpl_->params_, 1 /*presentationContextId*/, + as, &ts[0], ts.size(), role), + remoteAet_, "initializing"); + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); } } - + static bool IsGenericTransferSyntax(const std::string& syntax) { @@ -994,7 +1038,7 @@ } } - void DicomUserConnection::Open() + void DicomUserConnection::OpenInternal(Mode mode) { if (IsOpen()) { @@ -1034,7 +1078,7 @@ Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false), remoteAet_, "connecting"); - SetupPresentationContexts(preferredTransferSyntax_); + SetupPresentationContexts(mode, preferredTransferSyntax_); // Do the association Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_), @@ -1339,4 +1383,317 @@ remotePort_ == remote.GetPortNumber() && manufacturer_ == remote.GetManufacturer()); } + + + static void FillSopSequence(DcmDataset& dataset, + const DcmTagKey& tag, + const std::list<std::string>& sopClassUids, + const std::list<std::string>& sopInstanceUids, + bool hasFailureReason, + Uint16 failureReason) + { + assert(sopClassUids.size() == sopInstanceUids.size()); + + if (sopInstanceUids.empty()) + { + // Add an empty sequence + if (!dataset.insertEmptyElement(tag).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + std::list<std::string>::const_iterator currentClass = sopClassUids.begin(); + std::list<std::string>::const_iterator currentInstance = sopInstanceUids.begin(); + + while (currentClass != sopClassUids.end()) + { + std::auto_ptr<DcmItem> item(new DcmItem); + if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, currentClass->c_str()).good() || + !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, currentInstance->c_str()).good() || + (hasFailureReason && + !item->putAndInsertUint16(DCM_FailureReason, failureReason).good()) || + !dataset.insertSequenceItem(tag, item.release()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + ++currentClass; + ++currentInstance; + } + + for (size_t i = 0; i < sopClassUids.size(); i++) + { + } + } + } + + + + + void DicomUserConnection::ReportStorageCommitment( + const std::string& transactionUid, + const std::list<std::string>& successSopClassUids, + const std::list<std::string>& successSopInstanceUids, + const std::list<std::string>& failureSopClassUids, + const std::list<std::string>& failureSopInstanceUids) + { + if (successSopClassUids.size() != successSopInstanceUids.size() || + failureSopClassUids.size() != failureSopInstanceUids.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (IsOpen()) + { + Close(); + } + + try + { + OpenInternal(Mode_ReportStorageCommitment); + + /** + * N-EVENT-REPORT + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1 + * + * Status code: + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 + **/ + + /** + * Send the "EVENT_REPORT_RQ" request + **/ + + LOG(INFO) << "Reporting modality \"" << remoteAet_ + << "\" about storage commitment transaction: " << transactionUid + << " (" << successSopClassUids.size() << " successes, " + << failureSopClassUids.size() << " failures)"; + const DIC_US messageId = pimpl_->assoc_->nextMsgID++; + + { + T_DIMSE_Message message; + memset(&message, 0, sizeof(message)); + message.CommandField = DIMSE_N_EVENT_REPORT_RQ; + + T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ; + content.MessageID = messageId; + strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); + strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); + content.DataSetType = DIMSE_DATASET_PRESENT; + + DcmDataset dataset; + if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids, + successSopInstanceUids, false, 0); + + // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html + if (failureSopClassUids.empty()) + { + content.EventTypeID = 1; // "Storage Commitment Request Successful" + } + else + { + content.EventTypeID = 2; // "Storage Commitment Request Complete - Failures Exist" + + // Failure reason + // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 + FillSopSequence(dataset, DCM_FailedSOPSequence, failureSopClassUids, + failureSopInstanceUids, true, 0x0112 /* No such object instance == 274 */); + } + + int presID = ASC_findAcceptedPresentationContextID( + pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to send N-EVENT-REPORT request to AET: " + remoteAet_); + } + + if (!DIMSE_sendMessageUsingMemoryData( + pimpl_->assoc_, presID, &message, NULL /* status detail */, + &dataset, NULL /* callback */, NULL /* callback context */, + NULL /* commandSet */).good()) + { + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + /** + * Read the "EVENT_REPORT_RSP" response + **/ + + { + T_ASC_PresentationContextID presID = 0; + T_DIMSE_Message message; + + if (!DIMSE_receiveCommand(pimpl_->assoc_, DIMSE_NONBLOCKING, 1, &presID, + &message, NULL /* no statusDetail */).good() || + message.CommandField != DIMSE_N_EVENT_REPORT_RSP) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to read N-EVENT-REPORT response from AET: " + remoteAet_); + } + + const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP; + if (content.MessageIDBeingRespondedTo != messageId || + !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) || + !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) || + //(content.opts & O_NEVENTREPORT_EVENTTYPEID) || // Pedantic test - The "content.EventTypeID" is not used by Orthanc + std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || + std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || + content.DataSetType != DIMSE_DATASET_NULL) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Badly formatted N-EVENT-REPORT response from AET: " + remoteAet_); + } + + if (content.DimseStatus != 0 /* success */) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "The request cannot be handled by remote AET: " + remoteAet_); + } + } + + Close(); + } + catch (OrthancException&) + { + Close(); + throw; + } + } + + + + void DicomUserConnection::RequestStorageCommitment( + const std::string& transactionUid, + const std::list<std::string>& sopClassUids, + const std::list<std::string>& sopInstanceUids) + { + if (sopClassUids.size() != sopInstanceUids.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (transactionUid.size() < 5 || + transactionUid.substr(0, 5) != "2.25.") + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (IsOpen()) + { + Close(); + } + + try + { + OpenInternal(Mode_RequestStorageCommitment); + + /** + * N-ACTION + * 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 + * + * Status code: + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 + **/ + + /** + * Send the "N_ACTION_RQ" request + **/ + + LOG(INFO) << "Request to modality \"" << remoteAet_ + << "\" about storage commitment for " << sopClassUids.size() + << " instances, with transaction UID: " << transactionUid; + const DIC_US messageId = pimpl_->assoc_->nextMsgID++; + + { + T_DIMSE_Message message; + memset(&message, 0, sizeof(message)); + message.CommandField = DIMSE_N_ACTION_RQ; + + T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ; + content.MessageID = messageId; + strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); + strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); + content.ActionTypeID = 1; // "Request Storage Commitment" + content.DataSetType = DIMSE_DATASET_PRESENT; + + DcmDataset dataset; + if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, false, 0); + + int presID = ASC_findAcceptedPresentationContextID( + pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to send N-ACTION request to AET: " + remoteAet_); + } + + if (!DIMSE_sendMessageUsingMemoryData( + pimpl_->assoc_, presID, &message, NULL /* status detail */, + &dataset, NULL /* callback */, NULL /* callback context */, + NULL /* commandSet */).good()) + { + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + /** + * Read the "N_ACTION_RSP" response + **/ + + { + T_ASC_PresentationContextID presID = 0; + T_DIMSE_Message message; + + if (!DIMSE_receiveCommand(pimpl_->assoc_, DIMSE_NONBLOCKING, 1, &presID, + &message, NULL /* no statusDetail */).good() || + message.CommandField != DIMSE_N_ACTION_RSP) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to read N-ACTION response from AET: " + remoteAet_); + } + + const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP; + if (content.MessageIDBeingRespondedTo != messageId || + !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) || + !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) || + //(content.opts & O_NACTION_ACTIONTYPEID) || // Pedantic test - The "content.ActionTypeID" is not used by Orthanc + std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || + std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || + content.DataSetType != DIMSE_DATASET_NULL) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Badly formatted N-ACTION response from AET: " + remoteAet_); + } + + if (content.DimseStatus != 0 /* success */) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "The request cannot be handled by remote AET: " + remoteAet_); + } + } + + Close(); + } + catch (OrthancException&) + { + Close(); + throw; + } + } }
--- a/Core/DicomNetworking/DicomUserConnection.h Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/DicomNetworking/DicomUserConnection.h Tue Feb 04 08:22:22 2020 +0100 @@ -54,6 +54,13 @@ struct PImpl; boost::shared_ptr<PImpl> pimpl_; + enum Mode + { + Mode_Generic, + Mode_ReportStorageCommitment, + Mode_RequestStorageCommitment + }; + // Connection parameters std::string preferredTransferSyntax_; std::string localAet_; @@ -67,7 +74,8 @@ void CheckIsOpen() const; - void SetupPresentationContexts(const std::string& preferredTransferSyntax); + void SetupPresentationContexts(Mode mode, + const std::string& preferredTransferSyntax); void MoveInternal(const std::string& targetAet, ResourceType level, @@ -79,6 +87,8 @@ void DefaultSetup(); + void OpenInternal(Mode mode); + public: DicomUserConnection(); @@ -137,7 +147,10 @@ void AddStorageSOPClass(const char* sop); - void Open(); + void Open() + { + OpenInternal(Mode_Generic); + } void Close(); @@ -212,5 +225,18 @@ bool IsSameAssociation(const std::string& localAet, const RemoteModalityParameters& remote) const; + + void ReportStorageCommitment( + const std::string& transactionUid, + const std::list<std::string>& successSopClassUids, + const std::list<std::string>& successSopInstanceUids, + const std::list<std::string>& failureSopClassUids, + const std::list<std::string>& failureSopInstanceUids); + + // transactionUid: To be generated by Toolbox::GenerateDicomPrivateUniqueIdentifier() + void RequestStorageCommitment( + const std::string& transactionUid, + const std::list<std::string>& sopClassUids, + const std::list<std::string>& sopInstanceUids); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IStorageCommitmentRequestHandler.h Tue Feb 04 08:22:22 2020 +0100 @@ -0,0 +1,65 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <boost/noncopyable.hpp> +#include <string> +#include <vector> + +namespace Orthanc +{ + class IStorageCommitmentRequestHandler : public boost::noncopyable + { + public: + virtual ~IStorageCommitmentRequestHandler() + { + } + + virtual void HandleRequest(const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; + + virtual void HandleReport(const std::string& transactionUid, + const std::vector<std::string>& successSopClassUids, + const std::vector<std::string>& successSopInstanceUids, + const std::vector<std::string>& failedSopClassUids, + const std::vector<std::string>& failedSopInstanceUids, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h Tue Feb 04 08:22:22 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-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "IStorageCommitmentRequestHandler.h" + +namespace Orthanc +{ + class IStorageCommitmentRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IStorageCommitmentRequestHandlerFactory() + { + } + + virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() = 0; + }; +}
--- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -92,9 +92,12 @@ #include "MoveScp.h" #include "../../Toolbox.h" #include "../../Logging.h" +#include "../../OrthancException.h" +#include <dcmtk/dcmdata/dcdeftag.h> /* for storage commitment */ +#include <dcmtk/dcmdata/dcsequen.h> /* for class DcmSequenceOfItems */ +#include <dcmtk/dcmdata/dcuid.h> /* for variable dcmAllStorageSOPClassUIDs */ #include <dcmtk/dcmnet/dcasccfg.h> /* for class DcmAssociationConfiguration */ -#include <dcmtk/dcmdata/dcuid.h> /* for variable dcmAllStorageSOPClassUIDs */ #include <boost/lexical_cast.hpp> @@ -271,33 +274,6 @@ OFString sprofile; OFString temp_str; - std::vector<const char*> knownAbstractSyntaxes; - - // For C-STORE - if (server.HasStoreRequestHandlerFactory()) - { - knownAbstractSyntaxes.push_back(UID_VerificationSOPClass); - } - - // For C-FIND - if (server.HasFindRequestHandlerFactory()) - { - knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel); - knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); - } - - if (server.HasWorklistRequestHandlerFactory()) - { - knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel); - } - - // For C-MOVE - if (server.HasMoveRequestHandlerFactory()) - { - knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); - knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel); - } - cond = ASC_receiveAssociation(net, &assoc, /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, NULL, NULL, @@ -361,133 +337,193 @@ << " on IP " << remoteIp; - std::vector<const char*> transferSyntaxes; - - // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1 - transferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); - transferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); - transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); - - // New transfer syntaxes supported since Orthanc 0.7.2 - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated)) { - transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); - } + /* accept the abstract syntaxes for C-ECHO, C-FIND, C-MOVE, + and storage commitment, if presented */ - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg)) - { - transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax); - } + std::vector<const char*> genericTransferSyntaxes; + genericTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); + genericTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); + genericTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000)) - { - transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax); - } + std::vector<const char*> knownAbstractSyntaxes; - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless)) - { - transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax); - transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax); - } + // For C-ECHO (always enabled since Orthanc 1.6.0; in earlier + // versions, only enabled if C-STORE was also enabled) + knownAbstractSyntaxes.push_back(UID_VerificationSOPClass); - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip)) - { - transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); - transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); - } - - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2)) - { - transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax); - transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax); - } + // For C-FIND + if (server.HasFindRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel); + knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); + } - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle)) - { - transferSyntaxes.push_back(UID_RLELosslessTransferSyntax); - } - - /* accept the Verification SOP Class if presented */ - cond = ASC_acceptContextsWithPreferredTransferSyntaxes( - assoc->params, - &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), - &transferSyntaxes[0], transferSyntaxes.size()); - if (cond.bad()) - { - LOG(INFO) << cond.text(); - AssociationCleanup(assoc); - return NULL; - } + if (server.HasWorklistRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel); + } - /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */ - size_t count = 0; - while (dcmAllStorageSOPClassUIDs[count] != NULL) - { - count++; - } + // For C-MOVE + if (server.HasMoveRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); + knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel); + } -#if DCMTK_VERSION_NUMBER >= 362 - // The global variable "numberOfDcmAllStorageSOPClassUIDs" is - // only published if DCMTK >= 3.6.2: - // https://bitbucket.org/sjodogne/orthanc/issues/137 - assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs); -#endif - - cond = ASC_acceptContextsWithPreferredTransferSyntaxes( - assoc->params, - dcmAllStorageSOPClassUIDs, count, - &transferSyntaxes[0], transferSyntaxes.size()); - if (cond.bad()) - { - LOG(INFO) << cond.text(); - AssociationCleanup(assoc); - return NULL; - } - - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet)) - { - /* - * Promiscous mode is enabled: Accept everything not known not - * to be a storage SOP class. - **/ - cond = acceptUnknownContextsWithPreferredTransferSyntaxes( - assoc->params, &transferSyntaxes[0], transferSyntaxes.size(), ASC_SC_ROLE_DEFAULT); + cond = ASC_acceptContextsWithPreferredTransferSyntaxes( + assoc->params, + &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), + &genericTransferSyntaxes[0], genericTransferSyntaxes.size()); if (cond.bad()) { LOG(INFO) << cond.text(); AssociationCleanup(assoc); return NULL; } + + + /* storage commitment support, new in Orthanc 1.6.0 */ + if (server.HasStorageCommitmentRequestHandlerFactory()) + { + /** + * "ASC_SC_ROLE_SCUSCP": The "SCU" role is needed to accept + * remote storage commitment requests, and the "SCP" role is + * needed to receive storage commitments answers. + **/ + const char* as[1] = { UID_StorageCommitmentPushModelSOPClass }; + cond = ASC_acceptContextsWithPreferredTransferSyntaxes( + assoc->params, as, 1, + &genericTransferSyntaxes[0], genericTransferSyntaxes.size(), ASC_SC_ROLE_SCUSCP); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + } + } + + + { + /* accept the abstract syntaxes for C-STORE, if presented */ + + std::vector<const char*> storageTransferSyntaxes; + + // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1 + storageTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); + storageTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); + storageTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); + + // New transfer syntaxes supported since Orthanc 0.7.2 + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated)) + { + storageTransferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg)) + { + storageTransferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000)) + { + storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless)) + { + storageTransferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip)) + { + storageTransferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2)) + { + storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle)) + { + storageTransferSyntaxes.push_back(UID_RLELosslessTransferSyntax); + } + + /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */ + size_t count = 0; + while (dcmAllStorageSOPClassUIDs[count] != NULL) + { + count++; + } + +#if DCMTK_VERSION_NUMBER >= 362 + // The global variable "numberOfDcmAllStorageSOPClassUIDs" is + // only published if DCMTK >= 3.6.2: + // https://bitbucket.org/sjodogne/orthanc/issues/137 + assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs); +#endif + + cond = ASC_acceptContextsWithPreferredTransferSyntaxes( + assoc->params, + dcmAllStorageSOPClassUIDs, count, + &storageTransferSyntaxes[0], storageTransferSyntaxes.size()); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet)) + { + /* + * Promiscous mode is enabled: Accept everything not known not + * to be a storage SOP class. + **/ + cond = acceptUnknownContextsWithPreferredTransferSyntaxes( + assoc->params, &storageTransferSyntaxes[0], storageTransferSyntaxes.size(), ASC_SC_ROLE_DEFAULT); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + } } /* set our app title */ @@ -689,6 +725,16 @@ supported = true; break; + case DIMSE_N_ACTION_RQ: + request = DicomRequestType_NAction; + supported = true; + break; + + case DIMSE_N_EVENT_REPORT_RQ: + request = DicomRequestType_NEventReport; + supported = true; + break; + default: // we cannot handle this kind of message cond = DIMSE_BADCOMMANDTYPE; @@ -770,6 +816,14 @@ } break; + case DicomRequestType_NAction: + cond = NActionScp(&msg, presID); + break; + + case DicomRequestType_NEventReport: + cond = NEventReportScp(&msg, presID); + break; + default: // Should never happen break; @@ -823,5 +877,352 @@ } return cond; } + + + static DcmDataset* ReadDataset(T_ASC_Association* assoc, + const char* errorMessage) + { + 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() || + tmp == NULL) + { + throw OrthancException(ErrorCode_NetworkProtocol, errorMessage); + } + + return tmp; + } + + + static std::string ReadString(DcmDataset& dataset, + const DcmTagKey& tag) + { + const char* s = NULL; + if (!dataset.findAndGetString(tag, s).good() || + s == NULL) + { + char buf[64]; + sprintf(buf, "Missing mandatory tag in dataset: (%04X,%04X)", + tag.getGroup(), tag.getElement()); + throw OrthancException(ErrorCode_NetworkProtocol, buf); + } + + return std::string(s); + } + + + static void ReadSopSequence(std::vector<std::string>& sopClassUids, + std::vector<std::string>& sopInstanceUids, + DcmDataset& dataset, + const DcmTagKey& tag, + bool mandatory) + { + sopClassUids.clear(); + sopInstanceUids.clear(); + + DcmSequenceOfItems* sequence = NULL; + if (!dataset.findAndGetSequence(tag, sequence).good() || + sequence == NULL) + { + if (mandatory) + { + char buf[64]; + sprintf(buf, "Missing mandatory sequence in dataset: (%04X,%04X)", + tag.getGroup(), tag.getElement()); + throw OrthancException(ErrorCode_NetworkProtocol, buf); + } + else + { + return; + } + } + + sopClassUids.reserve(sequence->card()); + sopInstanceUids.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) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Missing Referenced SOP Class/Instance UID " + "in storage commitment request"); + } + + sopClassUids.push_back(a); + sopInstanceUids.push_back(b); + } + } + + + 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<DcmDataset> dataset( + ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP")); + + std::string transactionUid = ReadString(*dataset, DCM_TransactionUID); + + std::vector<std::string> sopClassUid, sopInstanceUid; + ReadSopSequence(sopClassUid, sopInstanceUid, + *dataset, DCM_ReferencedSOPSequence, true /* mandatory */); + + LOG(INFO) << "Incoming storage commitment request, with transaction UID: " << transactionUid; + + for (size_t i = 0; i < sopClassUid.size(); i++) + { + LOG(INFO) << " (" << (i + 1) << "/" << sopClassUid.size() + << ") queried SOP Class/Instance UID: " + << sopClassUid[i] << " / " << sopInstanceUid[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<IStorageCommitmentRequestHandler> handler + (server_.GetStorageCommitmentRequestHandlerFactory(). + ConstructStorageCommitmentRequestHandler()); + + handler->HandleRequest(transactionUid, sopClassUid, sopInstanceUid, + 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 */); + } + } + + + OFCondition CommandDispatcher::NEventReportScp(T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID) + { + /** + * Starting with Orthanc 1.6.0, handling N-EVENT-REPORT for + * storage commitment. + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1 + **/ + + if (msg->CommandField != DIMSE_N_EVENT_REPORT_RQ /* value == 256 == 0x0100 */ || + !server_.HasStorageCommitmentRequestHandlerFactory()) + { + throw OrthancException(ErrorCode_InternalError); + } + + + /** + * Check that the storage commitment report is correctly formatted. + **/ + + const T_DIMSE_N_EventReportRQ& report = msg->msg.NEventReportRQ; + + if (report.EventTypeID != 1 /* successful */ && + report.EventTypeID != 2 /* failures exist */) + { + throw OrthancException(ErrorCode_NotImplemented, + "Unknown event for DICOM N-EVENT-REPORT SCP"); + } + + if (std::string(report.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || + std::string(report.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Unexpected incoming SOP class or instance UID for storage commitment"); + } + + if (report.DataSetType != DIMSE_DATASET_PRESENT) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Incoming storage commitment report 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-2. Storage Commitment Result - Event Information": + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html#table_J.3-2 + **/ + + std::auto_ptr<DcmDataset> dataset( + ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP")); + + std::string transactionUid = ReadString(*dataset, DCM_TransactionUID); + + std::vector<std::string> successSopClassUid, successSopInstanceUid; + ReadSopSequence(successSopClassUid, successSopInstanceUid, + *dataset, DCM_ReferencedSOPSequence, + (report.EventTypeID == 1) /* mandatory in the case of success */); + + std::vector<std::string> failedSopClassUid, failedSopInstanceUid; + + if (report.EventTypeID == 2 /* failures exist */) + { + ReadSopSequence(failedSopClassUid, failedSopInstanceUid, + *dataset, DCM_FailedSOPSequence, true); + } + + LOG(INFO) << "Incoming storage commitment report, with transaction UID: " << transactionUid; + + for (size_t i = 0; i < successSopClassUid.size(); i++) + { + LOG(INFO) << " (success " << (i + 1) << "/" << successSopClassUid.size() + << ") SOP Class/Instance UID: " + << successSopClassUid[i] << " / " << successSopInstanceUid[i]; + } + + for (size_t i = 0; i < failedSopClassUid.size(); i++) + { + LOG(INFO) << " (failure " << (i + 1) << "/" << failedSopClassUid.size() + << ") SOP Class/Instance UID: " + << failedSopClassUid[i] << " / " << failedSopInstanceUid[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<IStorageCommitmentRequestHandler> handler + (server_.GetStorageCommitmentRequestHandlerFactory(). + ConstructStorageCommitmentRequestHandler()); + + handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid, + failedSopClassUid, failedSopInstanceUid, + remoteIp_, remoteAet_, calledAet_); + + dimseStatus = 0; // Success + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error while processing an incoming storage commitment report: " << 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_EVENT_REPORT_RSP; + + T_DIMSE_N_EventReportRSP& content = response.msg.NEventReportRSP; + content.MessageIDBeingRespondedTo = report.MessageID; + strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); + content.DimseStatus = dimseStatus; + strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); + content.EventTypeID = 0; // Not present, as "O_NEVENTREPORT_EVENTTYPEID" not set in "opts" + content.DataSetType = DIMSE_DATASET_NULL; // Dataset is absent in storage commitment response + content.opts = O_NEVENTREPORT_AFFECTEDSOPCLASSUID | O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID; + + return DIMSE_sendMessageUsingMemoryData( + assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */, + NULL /* callback */, NULL /* callback context */, NULL /* commandSet */); + } + } } }
--- a/Core/DicomNetworking/Internals/CommandDispatcher.h Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.h Tue Feb 04 08:22:22 2020 +0100 @@ -56,6 +56,12 @@ std::string calledAet_; IApplicationEntityFilter* filter_; + OFCondition NActionScp(T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID); + + OFCondition NEventReportScp(T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID); + public: CommandDispatcher(const DicomServer& server, T_ASC_Association* assoc, @@ -69,11 +75,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); } }
--- a/Core/DicomNetworking/RemoteModalityParameters.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/DicomNetworking/RemoteModalityParameters.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -48,6 +48,9 @@ static const char* KEY_ALLOW_GET = "AllowGet"; static const char* KEY_ALLOW_MOVE = "AllowMove"; static const char* KEY_ALLOW_STORE = "AllowStore"; +static const char* KEY_ALLOW_N_ACTION = "AllowNAction"; +static const char* KEY_ALLOW_N_EVENT_REPORT = "AllowEventReport"; +static const char* KEY_ALLOW_STORAGE_COMMITMENT = "AllowStorageCommitment"; static const char* KEY_HOST = "Host"; static const char* KEY_MANUFACTURER = "Manufacturer"; static const char* KEY_PORT = "Port"; @@ -66,6 +69,8 @@ allowFind_ = true; allowMove_ = true; allowGet_ = true; + allowNAction_ = true; // For storage commitment + allowNEventReport_ = true; // For storage commitment } @@ -211,6 +216,23 @@ { allowMove_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_MOVE); } + + if (serialized.isMember(KEY_ALLOW_N_ACTION)) + { + allowNAction_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_ACTION); + } + + if (serialized.isMember(KEY_ALLOW_N_EVENT_REPORT)) + { + allowNEventReport_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_EVENT_REPORT); + } + + if (serialized.isMember(KEY_ALLOW_STORAGE_COMMITMENT)) + { + bool allow = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_STORAGE_COMMITMENT); + allowNAction_ = allow; + allowNEventReport_ = allow; + } } @@ -233,6 +255,12 @@ case DicomRequestType_Store: return allowStore_; + case DicomRequestType_NAction: + return allowNAction_; + + case DicomRequestType_NEventReport: + return allowNEventReport_; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -264,6 +292,14 @@ allowStore_ = allowed; break; + case DicomRequestType_NAction: + allowNAction_ = allowed; + break; + + case DicomRequestType_NEventReport: + allowNEventReport_ = allowed; + break; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -276,7 +312,9 @@ !allowStore_ || !allowFind_ || !allowGet_ || - !allowMove_); + !allowMove_ || + !allowNAction_ || + !allowNEventReport_); } @@ -296,6 +334,8 @@ target[KEY_ALLOW_FIND] = allowFind_; target[KEY_ALLOW_GET] = allowGet_; target[KEY_ALLOW_MOVE] = allowMove_; + target[KEY_ALLOW_N_ACTION] = allowNAction_; + target[KEY_ALLOW_N_EVENT_REPORT] = allowNEventReport_; } else {
--- a/Core/DicomNetworking/RemoteModalityParameters.h Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/DicomNetworking/RemoteModalityParameters.h Tue Feb 04 08:22:22 2020 +0100 @@ -53,7 +53,9 @@ bool allowFind_; bool allowMove_; bool allowGet_; - + bool allowNAction_; + bool allowNEventReport_; + void Clear(); void UnserializeArray(const Json::Value& serialized);
--- a/Core/Enumerations.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/Enumerations.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -366,6 +366,9 @@ case ErrorCode_AlreadyExistingTag: return "Cannot override the value of a tag that already exists"; + case ErrorCode_NoStorageCommitmentHandler: + return "No request handler factory for DICOM N-ACTION SCP (storage commitment)"; + case ErrorCode_UnsupportedMediaType: return "Unsupported media type"; @@ -860,6 +863,14 @@ return "Store"; break; + case DicomRequestType_NAction: + return "N-ACTION"; + break; + + case DicomRequestType_NEventReport: + return "N-EVENT-REPORT"; + break; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); }
--- a/Core/Enumerations.h Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/Enumerations.h Tue Feb 04 08:22:22 2020 +0100 @@ -239,6 +239,7 @@ 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_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, + ErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, ErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, ErrorCode_START_PLUGINS = 1000000 }; @@ -622,7 +623,9 @@ DicomRequestType_Find, DicomRequestType_Get, DicomRequestType_Move, - DicomRequestType_Store + DicomRequestType_Store, + DicomRequestType_NAction, + DicomRequestType_NEventReport }; enum TransferSyntax
--- a/Core/HttpClient.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/HttpClient.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -234,7 +234,8 @@ if (sourceRemainingSize > 0) { // transmit the end of current source buffer - memcpy(curlBuffer + curlBufferFilledSize, sourceBuffer_.data() + sourceBufferTransmittedSize_, sourceRemainingSize); + memcpy(curlBuffer + curlBufferFilledSize, + sourceBuffer_.data() + sourceBufferTransmittedSize_, sourceRemainingSize); curlBufferFilledSize += sourceRemainingSize; } @@ -248,11 +249,13 @@ sourceRemainingSize = sourceBuffer_.size(); } - if (sourceRemainingSize > 0 && (curlBufferSize - curlBufferFilledSize) > 0) + if (sourceRemainingSize > 0 && + curlBufferSize > curlBufferFilledSize) { size_t s = std::min(sourceRemainingSize, curlBufferSize - curlBufferFilledSize); - memcpy(curlBuffer + curlBufferFilledSize, sourceBuffer_.data() + sourceBufferTransmittedSize_, s); + memcpy(curlBuffer + curlBufferFilledSize, + sourceBuffer_.data() + sourceBufferTransmittedSize_, s); sourceBufferTransmittedSize_ += s; curlBufferFilledSize += s;
--- a/Core/JobsEngine/JobsEngine.h Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/JobsEngine/JobsEngine.h Tue Feb 04 08:22:22 2020 +0100 @@ -39,7 +39,7 @@ namespace Orthanc { - class JobsEngine + class JobsEngine : public boost::noncopyable { private: enum State
--- a/Core/SerializationToolbox.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/SerializationToolbox.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -320,6 +320,28 @@ } + void WriteListOfStrings(Json::Value& target, + const std::list<std::string>& values, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::arrayValue; + + for (std::list<std::string>::const_iterator it = values.begin(); + it != values.end(); ++it) + { + value.append(*it); + } + } + + void WriteSetOfStrings(Json::Value& target, const std::set<std::string>& values, const std::string& field)
--- a/Core/SerializationToolbox.h Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/SerializationToolbox.h Tue Feb 04 08:22:22 2020 +0100 @@ -83,6 +83,10 @@ const std::vector<std::string>& values, const std::string& field); + void WriteListOfStrings(Json::Value& target, + const std::list<std::string>& values, + const std::string& field); + void WriteSetOfStrings(Json::Value& target, const std::set<std::string>& values, const std::string& field);
--- a/Core/Toolbox.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/Toolbox.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -2073,6 +2073,108 @@ throw OrthancException(ErrorCode_BadFileFormat, "Invalid UTF-8 string"); } } + + + std::string Toolbox::LargeHexadecimalToDecimal(const std::string& hex) + { + /** + * NB: Focus of the code below is *not* efficiency, but + * readability! + **/ + + for (size_t i = 0; i < hex.size(); i++) + { + const char c = hex[i]; + if (!((c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f') || + (c >= '0' && c <= '9'))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Not an hexadecimal number"); + } + } + + std::vector<uint8_t> decimal; + decimal.push_back(0); + + for (size_t i = 0; i < hex.size(); i++) + { + uint8_t hexDigit = static_cast<uint8_t>(Hex2Dec(hex[i])); + assert(hexDigit <= 15); + + for (size_t j = 0; j < decimal.size(); j++) + { + uint8_t val = static_cast<uint8_t>(decimal[j]) * 16 + hexDigit; // Maximum: 9 * 16 + 15 + assert(val <= 159 /* == 9 * 16 + 15 */); + + decimal[j] = val % 10; + hexDigit = val / 10; + assert(hexDigit <= 15 /* == 159 / 10 */); + } + + while (hexDigit > 0) + { + decimal.push_back(hexDigit % 10); + hexDigit /= 10; + } + } + + size_t start = 0; + while (start < decimal.size() && + decimal[start] == '0') + { + start++; + } + + std::string s; + s.reserve(decimal.size() - start); + + for (size_t i = decimal.size(); i > start; i--) + { + s.push_back(decimal[i - 1] + '0'); + } + + return s; + } + + + std::string Toolbox::GenerateDicomPrivateUniqueIdentifier() + { + /** + * REFERENCE: "Creating a Privately Defined Unique Identifier + * (Informative)" / "UUID Derived UID" + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html + * https://stackoverflow.com/a/46316162/881731 + **/ + + std::string uuid = GenerateUuid(); + assert(IsUuid(uuid) && uuid.size() == 36); + + /** + * After removing the four dashes ("-") out of the 36-character + * UUID, we get a large hexadecimal number with 32 characters, + * each of those characters lying in the range [0,16[. The large + * number is thus in the [0,16^32[ = [0,256^16[ range. This number + * has a maximum of 39 decimal digits, as can be seen in Python: + * + * # python -c 'import math; print(math.log(16**32))/math.log(10))' + * 38.531839445 + * + * We now to convert the large hexadecimal number to a decimal + * number with up to 39 digits, remove the leading zeros, then + * prefix it with "2.25." + **/ + + // Remove the dashes + std::string hex = (uuid.substr(0, 8) + + uuid.substr(9, 4) + + uuid.substr(14, 4) + + uuid.substr(19, 4) + + uuid.substr(24, 12)); + assert(hex.size() == 32); + + return "2.25." + LargeHexadecimalToDecimal(hex); + } }
--- a/Core/Toolbox.h Tue Feb 04 08:22:02 2020 +0100 +++ b/Core/Toolbox.h Tue Feb 04 08:22:22 2020 +0100 @@ -257,6 +257,11 @@ size_t& utf8Length, const std::string& utf8, size_t position); + + std::string LargeHexadecimalToDecimal(const std::string& hex); + + // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html + std::string GenerateDicomPrivateUniqueIdentifier(); } }
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -1106,8 +1106,6 @@ static void PeerSystem(RestApiGetCall& call) { - ServerContext& context = OrthancRestApi::GetContext(call); - std::string remote = call.GetUriComponent("id", ""); OrthancConfiguration::ReaderLock lock; @@ -1275,7 +1273,7 @@ if (call.ParseJsonRequest(json)) { const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = + const RemoteModalityParameters remote = MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); std::auto_ptr<ParsedDicomFile> query @@ -1300,6 +1298,40 @@ } + static void TestStorageCommitment(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + Json::Value json; + if (call.ParseJsonRequest(json)) + { + const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); + const RemoteModalityParameters remote = + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + { + DicomUserConnection scu(localAet, remote); + + std::list<std::string> sopClassUids, sopInstanceUids; + sopClassUids.push_back("a"); + sopInstanceUids.push_back("b"); + sopClassUids.push_back("1.2.840.10008.5.1.4.1.1.6.1"); + sopInstanceUids.push_back("1.2.840.113543.6.6.4.7.64234348190163144631511103849051737563212"); + + std::string t = Toolbox::GenerateDicomPrivateUniqueIdentifier(); + scu.RequestStorageCommitment(t, sopClassUids, sopInstanceUids); + } + + Json::Value result; + call.GetOutput().AnswerJson(result); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object"); + } + } + + void OrthancRestApi::RegisterModalities() { Register("/modalities", ListModalities); @@ -1343,5 +1375,7 @@ Register("/peers/{id}/system", PeerSystem); Register("/modalities/{id}/find-worklist", DicomFindWorklist); + + Register("/modalities/{id}/storage-commitment", TestStorageCommitment); } }
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -49,10 +49,11 @@ #include "DicomModalityStoreJob.h" #include "DicomMoveScuJob.h" +#include "MergeStudyJob.h" #include "OrthancPeerStoreJob.h" #include "ResourceModificationJob.h" -#include "MergeStudyJob.h" #include "SplitStudyJob.h" +#include "StorageCommitmentScpJob.h" namespace Orthanc @@ -96,6 +97,10 @@ { return new DicomMoveScuJob(context_, source); } + else if (type == "StorageCommitmentScp") + { + return new StorageCommitmentScpJob(context_, source); + } else { return GenericJobUnserializer::UnserializeJob(source);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -0,0 +1,303 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "StorageCommitmentScpJob.h" + +#include "../../Core/DicomNetworking/DicomUserConnection.h" +#include "../../Core/Logging.h" +#include "../../Core/OrthancException.h" +#include "../../Core/SerializationToolbox.h" +#include "../OrthancConfiguration.h" +#include "../ServerContext.h" + + +static const char* ANSWER = "Answer"; +static const char* CALLED_AET = "CalledAet"; +static const char* FAILED_SOP_CLASS_UIDS = "FailedSopClassUids"; +static const char* FAILED_SOP_INSTANCE_UIDS = "FailedSopInstanceUids"; +static const char* LOOKUP = "Lookup"; +static const char* REMOTE_MODALITY = "RemoteModality"; +static const char* SOP_CLASS_UID = "SopClassUid"; +static const char* SOP_INSTANCE_UID = "SopInstanceUid"; +static const char* SUCCESS_SOP_CLASS_UIDS = "SuccessSopClassUids"; +static const char* SUCCESS_SOP_INSTANCE_UIDS = "SuccessSopInstanceUids"; +static const char* TRANSACTION_UID = "TransactionUid"; +static const char* TYPE = "Type"; + + + +namespace Orthanc +{ + class StorageCommitmentScpJob::LookupCommand : public SetOfCommandsJob::ICommand + { + private: + StorageCommitmentScpJob& that_; + std::string sopClassUid_; + std::string sopInstanceUid_; + + public: + LookupCommand(StorageCommitmentScpJob& that, + const std::string& sopClassUid, + const std::string& sopInstanceUid) : + that_(that), + sopClassUid_(sopClassUid), + sopInstanceUid_(sopInstanceUid) + { + } + + virtual bool Execute() + { + that_.LookupInstance(sopClassUid_, sopInstanceUid_); + return true; + } + + virtual void Serialize(Json::Value& target) const + { + target = Json::objectValue; + target[TYPE] = LOOKUP; + target[SOP_CLASS_UID] = sopClassUid_; + target[SOP_INSTANCE_UID] = sopInstanceUid_; + } + }; + + + class StorageCommitmentScpJob::AnswerCommand : public SetOfCommandsJob::ICommand + { + private: + StorageCommitmentScpJob& that_; + + public: + AnswerCommand(StorageCommitmentScpJob& that) : + that_(that) + { + if (that_.ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + that_.ready_ = true; + } + } + + virtual bool Execute() + { + that_.Answer(); + return true; + } + + virtual void Serialize(Json::Value& target) const + { + target = Json::objectValue; + target[TYPE] = ANSWER; + } + }; + + + class StorageCommitmentScpJob::Unserializer : public SetOfCommandsJob::ICommandUnserializer + { + private: + StorageCommitmentScpJob& that_; + + public: + Unserializer(StorageCommitmentScpJob& that) : + that_(that) + { + that_.ready_ = false; + } + + virtual ICommand* Unserialize(const Json::Value& source) const + { + const std::string type = SerializationToolbox::ReadString(source, TYPE); + + if (type == LOOKUP) + { + return new LookupCommand(that_, + SerializationToolbox::ReadString(source, SOP_CLASS_UID), + SerializationToolbox::ReadString(source, SOP_INSTANCE_UID)); + } + else if (type == ANSWER) + { + return new AnswerCommand(that_); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + }; + + + void StorageCommitmentScpJob::LookupInstance(const std::string& sopClassUid, + const std::string& sopInstanceUid) + { + bool success = false; + + try + { + std::vector<std::string> orthancId; + context_.GetIndex().LookupIdentifierExact(orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid); + + if (orthancId.size() == 1) + { + std::string a, b; + + // Make sure that the DICOM file can be re-read by DCMTK + // from the file storage, and that the actual SOP + // class/instance UIDs do match + ServerContext::DicomCacheLocker locker(context_, orthancId[0]); + if (locker.GetDicom().GetTagValue(a, DICOM_TAG_SOP_CLASS_UID) && + locker.GetDicom().GetTagValue(b, DICOM_TAG_SOP_INSTANCE_UID) && + a == sopClassUid && + b == sopInstanceUid) + { + success = true; + } + } + } + catch (OrthancException&) + { + } + + LOG(INFO) << " Storage commitment SCP job: " << (success ? "Success" : "Failure") + << " while looking for " << sopClassUid << " / " << sopInstanceUid; + + if (success) + { + successSopClassUids_.push_back(sopClassUid); + successSopInstanceUids_.push_back(sopInstanceUid); + } + else + { + failedSopClassUids_.push_back(sopClassUid); + failedSopInstanceUids_.push_back(sopInstanceUid); + } + } + + + void StorageCommitmentScpJob::Answer() + { + LOG(INFO) << " Storage commitment SCP job: Sending answer"; + + DicomUserConnection scu(calledAet_, remoteModality_); + scu.ReportStorageCommitment(transactionUid_, successSopClassUids_, successSopInstanceUids_, + failedSopClassUids_, failedSopInstanceUids_); + } + + + StorageCommitmentScpJob::StorageCommitmentScpJob(ServerContext& context, + const std::string& transactionUid, + const std::string& remoteAet, + const std::string& calledAet) : + context_(context), + ready_(false), + transactionUid_(transactionUid), + calledAet_(calledAet) + { + { + OrthancConfiguration::ReaderLock lock; + if (!lock.GetConfiguration().LookupDicomModalityUsingAETitle(remoteModality_, remoteAet)) + { + throw OrthancException(ErrorCode_InexistentItem, + "Unknown remote modality for storage commitment SCP: " + remoteAet); + } + } + } + + + void StorageCommitmentScpJob::AddInstance(const std::string& sopClassUid, + const std::string& sopInstanceUid) + { + if (ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + AddCommand(new LookupCommand(*this, sopClassUid, sopInstanceUid)); + } + } + + + void StorageCommitmentScpJob::MarkAsReady() + { + AddCommand(new AnswerCommand(*this)); + } + + + void StorageCommitmentScpJob::GetPublicContent(Json::Value& value) + { + SetOfCommandsJob::GetPublicContent(value); + + value["CalledAet"] = calledAet_; + value["RemoteAet"] = remoteModality_.GetApplicationEntityTitle(); + value["TransactionUid"] = transactionUid_; + } + + + + StorageCommitmentScpJob::StorageCommitmentScpJob(ServerContext& context, + const Json::Value& serialized) : + SetOfCommandsJob(new Unserializer(*this), serialized), + context_(context) + { + transactionUid_ = SerializationToolbox::ReadString(serialized, TRANSACTION_UID); + remoteModality_ = RemoteModalityParameters(serialized[REMOTE_MODALITY]); + calledAet_ = SerializationToolbox::ReadString(serialized, CALLED_AET); + SerializationToolbox::ReadListOfStrings(successSopClassUids_, serialized, SUCCESS_SOP_CLASS_UIDS); + SerializationToolbox::ReadListOfStrings(successSopInstanceUids_, serialized, SUCCESS_SOP_INSTANCE_UIDS); + SerializationToolbox::ReadListOfStrings(failedSopClassUids_, serialized, FAILED_SOP_CLASS_UIDS); + SerializationToolbox::ReadListOfStrings(failedSopInstanceUids_, serialized, FAILED_SOP_INSTANCE_UIDS); + } + + + bool StorageCommitmentScpJob::Serialize(Json::Value& target) + { + if (!SetOfCommandsJob::Serialize(target)) + { + return false; + } + else + { + target[TRANSACTION_UID] = transactionUid_; + remoteModality_.Serialize(target[REMOTE_MODALITY], true /* force advanced format */); + target[CALLED_AET] = calledAet_; + SerializationToolbox::WriteListOfStrings(target, successSopClassUids_, SUCCESS_SOP_CLASS_UIDS); + SerializationToolbox::WriteListOfStrings(target, successSopInstanceUids_, SUCCESS_SOP_INSTANCE_UIDS); + SerializationToolbox::WriteListOfStrings(target, failedSopClassUids_, FAILED_SOP_CLASS_UIDS); + SerializationToolbox::WriteListOfStrings(target, failedSopInstanceUids_, FAILED_SOP_INSTANCE_UIDS); + return true; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/StorageCommitmentScpJob.h Tue Feb 04 08:22:22 2020 +0100 @@ -0,0 +1,93 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../Core/DicomNetworking/RemoteModalityParameters.h" +#include "../../Core/JobsEngine/SetOfCommandsJob.h" + +#include <list> + +namespace Orthanc +{ + class ServerContext; + + class StorageCommitmentScpJob : public SetOfCommandsJob + { + private: + class LookupCommand; + class AnswerCommand; + class Unserializer; + + ServerContext& context_; + bool ready_; + std::string transactionUid_; + RemoteModalityParameters remoteModality_; + std::string calledAet_; + std::list<std::string> successSopClassUids_; + std::list<std::string> successSopInstanceUids_; + std::list<std::string> failedSopClassUids_; + std::list<std::string> failedSopInstanceUids_; + + void LookupInstance(const std::string& sopClassUid, + const std::string& sopInstanceUid); + void Answer(); + + public: + StorageCommitmentScpJob(ServerContext& context, + const std::string& transactionUid, + const std::string& remoteAet, + const std::string& calledAet); + + StorageCommitmentScpJob(ServerContext& context, + const Json::Value& serialized); + + void AddInstance(const std::string& sopClassUid, + const std::string& sopInstanceUid); + + void MarkAsReady(); + + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE + { + } + + virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE + { + target = "StorageCommitmentScp"; + } + + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + + virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + }; +}
--- a/OrthancServer/main.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/OrthancServer/main.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -50,6 +50,7 @@ #include "OrthancInitialization.h" #include "OrthancMoveRequestHandler.h" #include "ServerContext.h" +#include "ServerJobs/StorageCommitmentScpJob.h" #include "ServerToolbox.h" using namespace Orthanc; @@ -58,11 +59,11 @@ class OrthancStoreRequestHandler : public IStoreRequestHandler { private: - ServerContext& server_; + ServerContext& context_; public: OrthancStoreRequestHandler(ServerContext& context) : - server_(context) + context_(context) { } @@ -84,8 +85,66 @@ toStore.SetJson(dicomJson); std::string id; - server_.Store(id, toStore); + context_.Store(id, toStore); + } + } +}; + + + +class OrthancStorageCommitmentRequestHandler : public IStorageCommitmentRequestHandler +{ +private: + ServerContext& context_; + +public: + OrthancStorageCommitmentRequestHandler(ServerContext& context) : + context_(context) + { + } + + virtual void HandleRequest(const std::string& transactionUid, + const std::vector<std::string>& referencedSopClassUids, + const std::vector<std::string>& referencedSopInstanceUids, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + if (referencedSopClassUids.size() != referencedSopInstanceUids.size()) + { + throw OrthancException(ErrorCode_InternalError); } + + std::auto_ptr<StorageCommitmentScpJob> job( + new StorageCommitmentScpJob(context_, transactionUid, remoteAet, calledAet)); + + for (size_t i = 0; i < referencedSopClassUids.size(); i++) + { + job->AddInstance(referencedSopClassUids[i], referencedSopInstanceUids[i]); + } + + job->MarkAsReady(); + + context_.GetJobsEngine().GetRegistry().Submit(job.release(), 0 /* default priority */); + } + + virtual void HandleReport(const std::string& transactionUid, + const std::vector<std::string>& successSopClassUids, + const std::vector<std::string>& successSopInstanceUids, + const std::vector<std::string>& failedSopClassUids, + const std::vector<std::string>& failedSopInstanceUids, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + printf("HANDLE REPORT\n"); + + /** + * "After the N-EVENT-REPORT has been sent, the Transaction UID is + * no longer active and shall not be reused for other + * transactions." + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html + **/ } }; @@ -113,7 +172,8 @@ class MyDicomServerFactory : public IStoreRequestHandlerFactory, public IFindRequestHandlerFactory, - public IMoveRequestHandlerFactory + public IMoveRequestHandlerFactory, + public IStorageCommitmentRequestHandlerFactory { private: ServerContext& context_; @@ -166,6 +226,11 @@ return new OrthancMoveRequestHandler(context_); } + virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() + { + return new OrthancStorageCommitmentRequestHandler(context_); + } + void Done() { } @@ -676,6 +741,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"); } @@ -970,6 +1036,7 @@ dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory); dicomServer.SetFindRequestHandlerFactory(serverFactory); + dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory); { OrthancConfiguration::ReaderLock lock;
--- a/Plugins/Include/orthanc/OrthancCPlugin.h Tue Feb 04 08:22:02 2020 +0100 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Tue Feb 04 08:22:22 2020 +0100 @@ -301,6 +301,7 @@ 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_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, + OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, OrthancPluginErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
--- a/Resources/Configuration.json Tue Feb 04 08:22:02 2020 +0100 +++ b/Resources/Configuration.json Tue Feb 04 08:22:22 2020 +0100 @@ -205,13 +205,13 @@ /** * By default, the Orthanc SCP accepts all DICOM commands (C-ECHO, - * C-STORE, C-FIND, C-MOVE) issued by the registered remote SCU - * modalities. Starting with Orthanc 1.5.0, it is possible to - * specify which DICOM commands are allowed, separately for each - * remote modality, using the syntax below. The "AllowEcho" (resp. - * "AllowStore") option only has an effect respectively if global - * option "DicomAlwaysAllowEcho" (resp. "DicomAlwaysAllowStore") - * is set to false. + * C-STORE, C-FIND, C-MOVE, and storage commitment) issued by the + * registered remote SCU modalities. Starting with Orthanc 1.5.0, + * it is possible to specify which DICOM commands are allowed, + * separately for each remote modality, using the syntax + * below. The "AllowEcho" (resp. "AllowStore") option only has an + * effect respectively if global option "DicomAlwaysAllowEcho" + * (resp. "DicomAlwaysAllowStore") is set to false. **/ //"untrusted" : { // "AET" : "ORTHANC", @@ -220,7 +220,8 @@ // "AllowEcho" : false, // "AllowFind" : false, // "AllowMove" : false, - // "AllowStore" : true + // "AllowStore" : true, + // "AllowStorageCommitment" : false // new in 1.6.0 //} },
--- a/Resources/ErrorCodes.json Tue Feb 04 08:22:02 2020 +0100 +++ b/Resources/ErrorCodes.json Tue Feb 04 08:22:22 2020 +0100 @@ -547,6 +547,11 @@ "Name": "AlreadyExistingTag", "Description": "Cannot override the value of a tag that already exists" }, + { + "Code": 2043, + "Name": "NoStorageCommitmentHandler", + "Description": "No request handler factory for DICOM N-ACTION SCP (storage commitment)" + },
--- a/UnitTestsSources/MultiThreadingTests.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/UnitTestsSources/MultiThreadingTests.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -1897,6 +1897,8 @@ ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); } s = Json::nullValue; @@ -1925,6 +1927,8 @@ ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); } s["Port"] = "46"; @@ -1944,8 +1948,10 @@ operations.insert(DicomRequestType_Get); operations.insert(DicomRequestType_Move); operations.insert(DicomRequestType_Store); + operations.insert(DicomRequestType_NAction); + operations.insert(DicomRequestType_NEventReport); - ASSERT_EQ(5u, operations.size()); + ASSERT_EQ(7u, operations.size()); for (std::set<DicomRequestType>::const_iterator it = operations.begin(); it != operations.end(); ++it) @@ -1974,4 +1980,54 @@ } } } + + { + Json::Value s; + s["AllowStorageCommitment"] = false; + s["AET"] = "AET"; + s["Host"] = "host"; + s["Port"] = "104"; + + RemoteModalityParameters modality(s); + ASSERT_TRUE(modality.IsAdvancedFormatNeeded()); + ASSERT_EQ("AET", modality.GetApplicationEntityTitle()); + ASSERT_EQ("host", modality.GetHost()); + ASSERT_EQ(104u, modality.GetPortNumber()); + ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + } + + { + Json::Value s; + s["AllowNAction"] = false; + s["AllowNEventReport"] = true; + s["AET"] = "AET"; + s["Host"] = "host"; + s["Port"] = "104"; + + RemoteModalityParameters modality(s); + ASSERT_TRUE(modality.IsAdvancedFormatNeeded()); + ASSERT_EQ("AET", modality.GetApplicationEntityTitle()); + ASSERT_EQ("host", modality.GetHost()); + ASSERT_EQ(104u, modality.GetPortNumber()); + ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + } + + { + Json::Value s; + s["AllowNAction"] = true; + s["AllowNEventReport"] = true; + s["AET"] = "AET"; + s["Host"] = "host"; + s["Port"] = "104"; + + RemoteModalityParameters modality(s); + ASSERT_FALSE(modality.IsAdvancedFormatNeeded()); + ASSERT_EQ("AET", modality.GetApplicationEntityTitle()); + ASSERT_EQ("host", modality.GetHost()); + ASSERT_EQ(104u, modality.GetPortNumber()); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); + ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + } }
--- a/UnitTestsSources/ToolboxTests.cpp Tue Feb 04 08:22:02 2020 +0100 +++ b/UnitTestsSources/ToolboxTests.cpp Tue Feb 04 08:22:22 2020 +0100 @@ -134,3 +134,26 @@ printf("decoding took %zu ms\n", (std::chrono::duration_cast<std::chrono::milliseconds>(afterDecoding - afterEncoding))); } #endif + + +TEST(Toolbox, LargeHexadecimalToDecimal) +{ + // https://stackoverflow.com/a/16967286/881731 + ASSERT_EQ( + "166089946137986168535368849184301740204613753693156360462575217560130904921953976324839782808018277000296027060873747803291797869684516494894741699267674246881622658654267131250470956587908385447044319923040838072975636163137212887824248575510341104029461758594855159174329892125993844566497176102668262139513", + Toolbox::LargeHexadecimalToDecimal("EC851A69B8ACD843164E10CFF70CF9E86DC2FEE3CF6F374B43C854E3342A2F1AC3E30C741CC41E679DF6D07CE6FA3A66083EC9B8C8BF3AF05D8BDBB0AA6Cb3ef8c5baa2a5e531ba9e28592f99e0fe4f95169a6c63f635d0197e325c5ec76219b907e4ebdcd401fb1986e4e3ca661ff73e7e2b8fd9988e753b7042b2bbca76679")); + + ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("")); + ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0")); + ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0000")); + ASSERT_EQ("255", Toolbox::LargeHexadecimalToDecimal("00000ff")); + + ASSERT_THROW(Toolbox::LargeHexadecimalToDecimal("g"), Orthanc::OrthancException); +} + + +TEST(Toolbox, GenerateDicomPrivateUniqueIdentifier) +{ + std::string s = Toolbox::GenerateDicomPrivateUniqueIdentifier(); + ASSERT_EQ("2.25.", s.substr(0, 5)); +}