Mercurial > hg > orthanc
changeset 3673:3e68c3b4e3df storage-commitment
integration mainline->storage-commitment
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 14 Feb 2020 17:55:53 +0100 |
parents | e64432336055 (diff) ea8c1c0e81eb (current diff) |
children | 9201a7858cce |
files | NEWS |
diffstat | 53 files changed, 2809 insertions(+), 254 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Fri Feb 14 17:22:28 2020 +0100 +++ b/CMakeLists.txt Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/DicomNetworking/DicomServer.cpp Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/DicomNetworking/DicomServer.h Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Fri Feb 14 17:55:53 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,354 @@ remotePort_ == remote.GetPortNumber() && manufacturer_ == remote.GetManufacturer()); } + + + static void FillSopSequence(DcmDataset& dataset, + const DcmTagKey& tag, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::vector<StorageCommitmentFailureReason>& failureReasons, + bool hasFailureReasons) + { + assert(sopClassUids.size() == sopInstanceUids.size() && + (hasFailureReasons ? + failureReasons.size() == sopClassUids.size() : + failureReasons.empty())); + + if (sopInstanceUids.empty()) + { + // Add an empty sequence + if (!dataset.insertEmptyElement(tag).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + for (size_t i = 0; i < sopClassUids.size(); i++) + { + std::auto_ptr<DcmItem> item(new DcmItem); + if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() || + !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() || + (hasFailureReasons && + !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) || + !dataset.insertSequenceItem(tag, item.release()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + } + + + + + void DicomUserConnection::ReportStorageCommitment( + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::vector<StorageCommitmentFailureReason>& failureReasons) + { + if (sopClassUids.size() != sopInstanceUids.size() || + sopClassUids.size() != failureReasons.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (IsOpen()) + { + Close(); + } + + std::vector<std::string> successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids; + std::vector<StorageCommitmentFailureReason> failedReasons; + + successSopClassUids.reserve(sopClassUids.size()); + successSopInstanceUids.reserve(sopClassUids.size()); + failedSopClassUids.reserve(sopClassUids.size()); + failedSopInstanceUids.reserve(sopClassUids.size()); + failedReasons.reserve(sopClassUids.size()); + + for (size_t i = 0; i < sopClassUids.size(); i++) + { + switch (failureReasons[i]) + { + case StorageCommitmentFailureReason_Success: + successSopClassUids.push_back(sopClassUids[i]); + successSopInstanceUids.push_back(sopInstanceUids[i]); + break; + + case StorageCommitmentFailureReason_ProcessingFailure: + case StorageCommitmentFailureReason_NoSuchObjectInstance: + case StorageCommitmentFailureReason_ResourceLimitation: + case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported: + case StorageCommitmentFailureReason_ClassInstanceConflict: + case StorageCommitmentFailureReason_DuplicateTransactionUID: + failedSopClassUids.push_back(sopClassUids[i]); + failedSopInstanceUids.push_back(sopInstanceUids[i]); + failedReasons.push_back(failureReasons[i]); + break; + + default: + { + char buf[16]; + sprintf(buf, "%04xH", failureReasons[i]); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unsupported failure reason for storage commitment: " + std::string(buf)); + } + } + } + + 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, " + << failedSopClassUids.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); + } + + { + std::vector<StorageCommitmentFailureReason> empty; + FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids, + successSopInstanceUids, empty, false); + } + + // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html + if (failedSopClassUids.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, failedSopClassUids, + failedSopInstanceUids, failedReasons, true); + } + + 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::vector<std::string>& sopClassUids, + const std::vector<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); + } + + { + std::vector<StorageCommitmentFailureReason> empty; + FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false); + } + + 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/DicomNetworking/DicomUserConnection.h Fri Feb 14 17:55:53 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,17 @@ bool IsSameAssociation(const std::string& localAet, const RemoteModalityParameters& remote) const; + + void ReportStorageCommitment( + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::vector<StorageCommitmentFailureReason>& failureReasons); + + // transactionUid: To be generated by Toolbox::GenerateDicomPrivateUniqueIdentifier() + void RequestStorageCommitment( + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IStorageCommitmentRequestHandler.h Fri Feb 14 17:55:53 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 Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Fri Feb 14 17:55:53 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,144 +337,204 @@ << " 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); - } - - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip)) - { - transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); - transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); - } + // 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_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); + } - // New in Orthanc 1.6.0 - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg4)) - { - transferSyntaxes.push_back(UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax); - transferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_1TransferSyntax); - transferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax); - transferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax); - transferSyntaxes.push_back(UID_MPEG4StereoHighProfileLevel4_2TransferSyntax); - } - - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle)) - { - transferSyntaxes.push_back(UID_RLELosslessTransferSyntax); - } + if (server.HasWorklistRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel); + } - /* 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; - } - - /* 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); + } + + // New in Orthanc 1.6.0 + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg4)) + { + storageTransferSyntaxes.push_back(UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_1TransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG4StereoHighProfileLevel4_2TransferSyntax); + } + + 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 */ @@ -700,6 +736,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; @@ -781,6 +827,14 @@ } break; + case DicomRequestType_NAction: + cond = NActionScp(&msg, presID); + break; + + case DicomRequestType_NEventReport: + cond = NEventReportScp(&msg, presID); + break; + default: // Should never happen break; @@ -834,5 +888,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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.h Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/DicomNetworking/RemoteModalityParameters.cpp Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/DicomNetworking/RemoteModalityParameters.h Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/Enumerations.cpp Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/Enumerations.h Fri Feb 14 17:55:53 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 @@ -664,6 +667,36 @@ JobStopReason_Retry }; + + // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 + enum StorageCommitmentFailureReason + { + StorageCommitmentFailureReason_Success = 0, + + // A general failure in processing the operation was encountered + StorageCommitmentFailureReason_ProcessingFailure = 0x0110, + + // One or more of the elements in the Referenced SOP Instance + // Sequence was not available + StorageCommitmentFailureReason_NoSuchObjectInstance = 0x0112, + + // The SCP does not currently have enough resources to store the + // requested SOP Instance(s) + StorageCommitmentFailureReason_ResourceLimitation = 0x0213, + + // Storage Commitment has been requested for a SOP Instance with a + // SOP Class that is not supported by the SCP + StorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 0x0122, + + // The SOP Class of an element in the Referenced SOP Instance + // Sequence did not correspond to the SOP class registered for + // this SOP Instance at the SCP + StorageCommitmentFailureReason_ClassInstanceConflict = 0x0119, + + // The Transaction UID of the Storage Commitment Request is already in use + StorageCommitmentFailureReason_DuplicateTransactionUID = 0x0131 + }; + /** * WARNING: Do not change the explicit values in the enumerations
--- a/Core/HttpClient.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/HttpClient.cpp Fri Feb 14 17:55:53 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/IJob.h Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/JobsEngine/IJob.h Fri Feb 14 17:55:53 2020 +0100 @@ -50,7 +50,7 @@ // Method called once the job enters the jobs engine virtual void Start() = 0; - virtual JobStepResult Step() = 0; + virtual JobStepResult Step(const std::string& jobId) = 0; // Method called once the job is resubmitted after a failure virtual void Reset() = 0;
--- a/Core/JobsEngine/JobsEngine.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/JobsEngine/JobsEngine.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -71,7 +71,7 @@ try { - result = running.GetJob().Step(); + result = running.GetJob().Step(running.GetId()); } catch (OrthancException& e) {
--- a/Core/JobsEngine/JobsEngine.h Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/JobsEngine/JobsEngine.h Fri Feb 14 17:55:53 2020 +0100 @@ -39,7 +39,7 @@ namespace Orthanc { - class JobsEngine + class JobsEngine : public boost::noncopyable { private: enum State
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -319,7 +319,7 @@ } - JobStepResult SequenceOfOperationsJob::Step() + JobStepResult SequenceOfOperationsJob::Step(const std::string& jobId) { boost::mutex::scoped_lock lock(mutex_);
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Fri Feb 14 17:55:53 2020 +0100 @@ -130,7 +130,7 @@ { } - virtual JobStepResult Step(); + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; virtual void Reset();
--- a/Core/JobsEngine/SetOfCommandsJob.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/JobsEngine/SetOfCommandsJob.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -145,7 +145,7 @@ } - JobStepResult SetOfCommandsJob::Step() + JobStepResult SetOfCommandsJob::Step(const std::string& jobId) { if (!started_) { @@ -169,7 +169,7 @@ try { // Not at the trailing step: Handle the current command - if (!commands_[position_]->Execute()) + if (!commands_[position_]->Execute(jobId)) { // Error if (!permissive_)
--- a/Core/JobsEngine/SetOfCommandsJob.h Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/JobsEngine/SetOfCommandsJob.h Fri Feb 14 17:55:53 2020 +0100 @@ -49,7 +49,7 @@ { } - virtual bool Execute() = 0; + virtual bool Execute(const std::string& jobId) = 0; virtual void Serialize(Json::Value& target) const = 0; }; @@ -126,7 +126,7 @@ const ICommand& GetCommand(size_t index) const; - virtual JobStepResult Step(); + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; virtual void GetPublicContent(Json::Value& value);
--- a/Core/JobsEngine/SetOfInstancesJob.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/JobsEngine/SetOfInstancesJob.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -60,7 +60,7 @@ return instance_; } - virtual bool Execute() + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE { if (!that_.HandleInstance(instance_)) { @@ -91,12 +91,12 @@ { } - virtual bool Execute() + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE { return that_.HandleTrailingStep(); } - virtual void Serialize(Json::Value& target) const + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE { target = Json::nullValue; }
--- a/Core/SerializationToolbox.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/SerializationToolbox.cpp Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/SerializationToolbox.h Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/Toolbox.cpp Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Core/Toolbox.h Fri Feb 14 17:55:53 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/NEWS Fri Feb 14 17:22:28 2020 +0100 +++ b/NEWS Fri Feb 14 17:55:53 2020 +0100 @@ -1,6 +1,11 @@ Pending changes in the mainline =============================== +General +------- + +* Support of DICOM storage commitment + REST API -------- @@ -15,6 +20,7 @@ ------- * New sample plugin: "ConnectivityChecks" +* New primitives to handle storage commitment SCP by plugins Maintenance -----------
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -1273,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 @@ -1298,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::vector<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); @@ -1341,5 +1375,7 @@ Register("/peers/{id}/system", PeerSystem); Register("/modalities/{id}/find-worklist", DicomFindWorklist); + + Register("/modalities/{id}/storage-commitment", TestStorageCommitment); } }
--- a/OrthancServer/ServerContext.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/OrthancServer/ServerContext.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -1052,4 +1052,24 @@ } #endif } + + + IStorageCommitmentFactory::ILookupHandler* + ServerContext::CreateStorageCommitment(const std::string& jobId, + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::string& remoteAet, + const std::string& calledAet) + { +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins()) + { + return GetPlugins().CreateStorageCommitment( + jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet); + } +#endif + + return NULL; + } }
--- a/OrthancServer/ServerContext.h Fri Feb 14 17:22:28 2020 +0100 +++ b/OrthancServer/ServerContext.h Fri Feb 14 17:55:53 2020 +0100 @@ -37,6 +37,7 @@ #include "LuaScripting.h" #include "OrthancHttpHandler.h" #include "ServerIndex.h" +#include "ServerJobs/IStorageCommitmentFactory.h" #include "../Core/Cache/MemoryCache.h" @@ -60,7 +61,9 @@ * filesystem (including compression), as well as the index of the * DICOM store. It implements the required locking mechanisms. **/ - class ServerContext : private JobsRegistry::IObserver + class ServerContext : + public IStorageCommitmentFactory, + private JobsRegistry::IObserver { public: class ILookupVisitor : public boost::noncopyable @@ -424,5 +427,13 @@ { return isExecuteLuaEnabled_; } + + virtual IStorageCommitmentFactory::ILookupHandler* + CreateStorageCommitment(const std::string& jobId, + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::string& remoteAet, + const std::string& calledAet) ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/ServerJobs/ArchiveJob.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/OrthancServer/ServerJobs/ArchiveJob.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -935,7 +935,7 @@ } - JobStepResult ArchiveJob::Step() + JobStepResult ArchiveJob::Step(const std::string& jobId) { assert(writer_.get() != NULL);
--- a/OrthancServer/ServerJobs/ArchiveJob.h Fri Feb 14 17:22:28 2020 +0100 +++ b/OrthancServer/ServerJobs/ArchiveJob.h Fri Feb 14 17:55:53 2020 +0100 @@ -92,7 +92,7 @@ virtual void Start(); - virtual JobStepResult Step(); + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; virtual void Stop(JobStopReason reason) {
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -57,7 +57,7 @@ { } - virtual bool Execute() + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE { that_.Retrieve(*findAnswer_); return true;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/IStorageCommitmentFactory.h Fri Feb 14 17:55:53 2020 +0100 @@ -0,0 +1,66 @@ +/** + * 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 <string> +#include <vector> + +namespace Orthanc +{ + class IStorageCommitmentFactory : public boost::noncopyable + { + public: + class ILookupHandler : public boost::noncopyable + { + public: + virtual ~ILookupHandler() + { + } + + virtual StorageCommitmentFailureReason Lookup(const std::string& sopClassUid, + const std::string& sopInstanceUid) = 0; + }; + + virtual ~IStorageCommitmentFactory() + { + } + + virtual ILookupHandler* CreateStorageCommitment(const std::string& jobId, + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::string& remoteAet, + const std::string& calledAet) = 0; + }; +}
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Fri Feb 14 17:55:53 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 Fri Feb 14 17:55:53 2020 +0100 @@ -0,0 +1,454 @@ +/** + * 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* INDEX = "Index"; +static const char* LOOKUP = "Lookup"; +static const char* REMOTE_MODALITY = "RemoteModality"; +static const char* SETUP = "Setup"; +static const char* SOP_CLASS_UIDS = "SopClassUids"; +static const char* SOP_INSTANCE_UIDS = "SopInstanceUids"; +static const char* TRANSACTION_UID = "TransactionUid"; +static const char* TYPE = "Type"; + + + +namespace Orthanc +{ + class StorageCommitmentScpJob::StorageCommitmentCommand : public SetOfCommandsJob::ICommand + { + public: + virtual CommandType GetType() const = 0; + }; + + + class StorageCommitmentScpJob::SetupCommand : public StorageCommitmentCommand + { + private: + StorageCommitmentScpJob& that_; + + public: + SetupCommand(StorageCommitmentScpJob& that) : + that_(that) + { + } + + virtual CommandType GetType() const + { + return CommandType_Setup; + } + + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE + { + that_.Setup(jobId); + return true; + } + + virtual void Serialize(Json::Value& target) const + { + target = Json::objectValue; + target[TYPE] = SETUP; + } + }; + + + class StorageCommitmentScpJob::LookupCommand : public StorageCommitmentCommand + { + private: + StorageCommitmentScpJob& that_; + size_t index_; + bool hasFailureReason_; + StorageCommitmentFailureReason failureReason_; + + public: + LookupCommand(StorageCommitmentScpJob& that, + size_t index) : + that_(that), + index_(index), + hasFailureReason_(false) + { + } + + virtual CommandType GetType() const + { + return CommandType_Lookup; + } + + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE + { + failureReason_ = that_.Lookup(index_); + hasFailureReason_ = true; + return true; + } + + size_t GetIndex() const + { + return index_; + } + + StorageCommitmentFailureReason GetFailureReason() const + { + if (hasFailureReason_) + { + return failureReason_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + virtual void Serialize(Json::Value& target) const + { + target = Json::objectValue; + target[TYPE] = LOOKUP; + target[INDEX] = static_cast<unsigned int>(index_); + } + }; + + + class StorageCommitmentScpJob::AnswerCommand : public StorageCommitmentCommand + { + private: + StorageCommitmentScpJob& that_; + + public: + AnswerCommand(StorageCommitmentScpJob& that) : + that_(that) + { + if (that_.ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + that_.ready_ = true; + } + } + + virtual CommandType GetType() const + { + return CommandType_Answer; + } + + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE + { + 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 == SETUP) + { + return new SetupCommand(that_); + } + else if (type == LOOKUP) + { + return new LookupCommand(that_, SerializationToolbox::ReadUnsignedInteger(source, INDEX)); + } + else if (type == ANSWER) + { + return new AnswerCommand(that_); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + }; + + + void StorageCommitmentScpJob::CheckInvariants() + { + const size_t n = GetCommandsCount(); + + if (n <= 1) + { + throw OrthancException(ErrorCode_InternalError); + } + + for (size_t i = 0; i < n; i++) + { + const CommandType type = dynamic_cast<const StorageCommitmentCommand&>(GetCommand(i)).GetType(); + + if ((i == 0 && type != CommandType_Setup) || + (i >= 1 && i < n - 1 && type != CommandType_Lookup) || + (i == n - 1 && type != CommandType_Answer)) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (type == CommandType_Lookup) + { + const LookupCommand& lookup = dynamic_cast<const LookupCommand&>(GetCommand(i)); + if (lookup.GetIndex() != i - 1) + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + } + + + void StorageCommitmentScpJob::Setup(const std::string& jobId) + { + CheckInvariants(); + + const std::string& remoteAet = remoteModality_.GetApplicationEntityTitle(); + lookupHandler_.reset(context_.CreateStorageCommitment(jobId, transactionUid_, sopClassUids_, + sopInstanceUids_, remoteAet, calledAet_)); + } + + + StorageCommitmentFailureReason StorageCommitmentScpJob::Lookup(size_t index) + { +#ifndef NDEBUG + CheckInvariants(); +#endif + + if (index >= sopClassUids_.size()) + { + throw OrthancException(ErrorCode_InternalError); + } + else if (lookupHandler_.get() != NULL) + { + return lookupHandler_->Lookup(sopClassUids_[index], sopInstanceUids_[index]); + } + else + { + // This is the default implementation of Orthanc (if no storage + // commitment plugin is installed) + bool success = false; + StorageCommitmentFailureReason reason = + StorageCommitmentFailureReason_NoSuchObjectInstance /* 0x0112 == 274 */; + + try + { + std::vector<std::string> orthancId; + context_.GetIndex().LookupIdentifierExact(orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids_[index]); + + 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) && + b == sopInstanceUids_[index]) + { + if (a == sopClassUids_[index]) + { + success = true; + reason = StorageCommitmentFailureReason_Success; + } + else + { + // Mismatch in the SOP class UID + reason = StorageCommitmentFailureReason_ClassInstanceConflict /* 0x0119 */; + } + } + } + } + catch (OrthancException&) + { + } + + LOG(INFO) << " Storage commitment SCP job: " << (success ? "Success" : "Failure") + << " while looking for " << sopClassUids_[index] << " / " << sopInstanceUids_[index]; + + return reason; + } + } + + + void StorageCommitmentScpJob::Answer() + { + CheckInvariants(); + LOG(INFO) << " Storage commitment SCP job: Sending answer"; + + std::vector<StorageCommitmentFailureReason> failureReasons; + failureReasons.reserve(sopClassUids_.size()); + + for (size_t i = 1; i < GetCommandsCount() - 1; i++) + { + const LookupCommand& lookup = dynamic_cast<const LookupCommand&>(GetCommand(i)); + failureReasons.push_back(lookup.GetFailureReason()); + } + + if (failureReasons.size() != sopClassUids_.size()) + { + throw OrthancException(ErrorCode_InternalError); + } + + DicomUserConnection scu(calledAet_, remoteModality_); + scu.ReportStorageCommitment(transactionUid_, sopClassUids_, sopInstanceUids_, failureReasons); + } + + + 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); + } + } + + AddCommand(new SetupCommand(*this)); + } + + + void StorageCommitmentScpJob::Reserve(size_t size) + { + if (ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + sopClassUids_.reserve(size); + sopInstanceUids_.reserve(size); + } + } + + + void StorageCommitmentScpJob::AddInstance(const std::string& sopClassUid, + const std::string& sopInstanceUid) + { + if (ready_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + assert(sopClassUids_.size() == sopInstanceUids_.size()); + AddCommand(new LookupCommand(*this, sopClassUids_.size())); + sopClassUids_.push_back(sopClassUid); + sopInstanceUids_.push_back(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::ReadArrayOfStrings(sopClassUids_, serialized, SOP_CLASS_UIDS); + SerializationToolbox::ReadArrayOfStrings(sopInstanceUids_, serialized, 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::WriteArrayOfStrings(target, sopClassUids_, SOP_CLASS_UIDS); + SerializationToolbox::WriteArrayOfStrings(target, sopInstanceUids_, SOP_INSTANCE_UIDS); + return true; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/StorageCommitmentScpJob.h Fri Feb 14 17:55:53 2020 +0100 @@ -0,0 +1,110 @@ +/** + * 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 "IStorageCommitmentFactory.h" + +#include <memory> +#include <vector> + +namespace Orthanc +{ + class ServerContext; + + class StorageCommitmentScpJob : public SetOfCommandsJob + { + private: + enum CommandType + { + CommandType_Setup, + CommandType_Lookup, + CommandType_Answer + }; + + class StorageCommitmentCommand; + class SetupCommand; + class LookupCommand; + class AnswerCommand; + class Unserializer; + + ServerContext& context_; + bool ready_; + std::string transactionUid_; + RemoteModalityParameters remoteModality_; + std::string calledAet_; + std::vector<std::string> sopClassUids_; + std::vector<std::string> sopInstanceUids_; + + std::auto_ptr<IStorageCommitmentFactory::ILookupHandler> lookupHandler_; + + void CheckInvariants(); + + void Setup(const std::string& jobId); + + StorageCommitmentFailureReason Lookup(size_t index); + + 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 Reserve(size_t size); + + 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/OrthancServer/main.cpp Fri Feb 14 17:55:53 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/Engine/OrthancPlugins.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Plugins/Engine/OrthancPlugins.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -682,6 +682,110 @@ }; + + class StorageCommitmentScp : public IStorageCommitmentFactory + { + private: + class Handler : public IStorageCommitmentFactory::ILookupHandler + { + private: + _OrthancPluginRegisterStorageCommitmentScpCallback parameters_; + void* handler_; + + public: + Handler(_OrthancPluginRegisterStorageCommitmentScpCallback parameters, + void* handler) : + parameters_(parameters), + handler_(handler) + { + if (handler == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + virtual ~Handler() + { + assert(handler_ != NULL); + parameters_.destructor(handler_); + handler_ = NULL; + } + + virtual StorageCommitmentFailureReason Lookup(const std::string& sopClassUid, + const std::string& sopInstanceUid) + { + assert(handler_ != NULL); + OrthancPluginStorageCommitmentFailureReason reason = + OrthancPluginStorageCommitmentFailureReason_Success; + OrthancPluginErrorCode error = parameters_.lookup( + &reason, handler_, sopClassUid.c_str(), sopInstanceUid.c_str()); + if (error == OrthancPluginErrorCode_Success) + { + return Plugins::Convert(reason); + } + else + { + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + }; + + _OrthancPluginRegisterStorageCommitmentScpCallback parameters_; + + public: + StorageCommitmentScp(_OrthancPluginRegisterStorageCommitmentScpCallback parameters) : + parameters_(parameters) + { + } + + virtual ILookupHandler* CreateStorageCommitment( + const std::string& jobId, + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::string& remoteAet, + const std::string& calledAet) ORTHANC_OVERRIDE + { + const size_t n = sopClassUids.size(); + + if (sopInstanceUids.size() != n) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + std::vector<const char*> a, b; + a.resize(n); + b.resize(n); + + for (size_t i = 0; i < n; i++) + { + a[i] = sopClassUids[i].c_str(); + b[i] = sopInstanceUids[i].c_str(); + } + + void* handler = NULL; + OrthancPluginErrorCode error = parameters_.factory( + &handler, jobId.c_str(), transactionUid.c_str(), + a.empty() ? NULL : &a[0], b.empty() ? NULL : &b[0], static_cast<uint32_t>(n), + remoteAet.c_str(), calledAet.c_str()); + + if (error != OrthancPluginErrorCode_Success) + { + throw OrthancException(static_cast<ErrorCode>(error)); + } + else if (handler == NULL) + { + // This plugin won't handle this storage commitment request + return NULL; + } + else + { + return new Handler(parameters_, handler); + } + } + }; + + class ServerContextLock { private: @@ -724,6 +828,7 @@ typedef std::list<OrthancPluginDecodeImageCallback> DecodeImageCallbacks; typedef std::list<OrthancPluginJobsUnserializer> JobsUnserializers; typedef std::list<OrthancPluginRefreshMetricsCallback> RefreshMetricsCallbacks; + typedef std::list<StorageCommitmentScp*> StorageCommitmentScpCallbacks; typedef std::map<Property, std::string> Properties; PluginsManager manager_; @@ -740,6 +845,7 @@ IncomingHttpRequestFilters incomingHttpRequestFilters_; IncomingHttpRequestFilters2 incomingHttpRequestFilters2_; RefreshMetricsCallbacks refreshMetricsCallbacks_; + StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_; std::auto_ptr<StorageAreaFactory> storageArea_; boost::recursive_mutex restCallbackMutex_; @@ -750,6 +856,7 @@ boost::mutex decodeImageCallbackMutex_; boost::mutex jobsUnserializersMutex_; boost::mutex refreshMetricsMutex_; + boost::mutex storageCommitmentScpMutex_; boost::recursive_mutex invokeServiceMutex_; Properties properties_; @@ -1260,6 +1367,7 @@ sizeof(int32_t) != sizeof(OrthancPluginConstraintType) || sizeof(int32_t) != sizeof(OrthancPluginMetricsType) || sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || + sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) || @@ -1303,6 +1411,13 @@ { delete *it; } + + for (PImpl::StorageCommitmentScpCallbacks::iterator + it = pimpl_->storageCommitmentScpCallbacks_.begin(); + it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it) + { + delete *it; + } } @@ -1863,6 +1978,18 @@ } + void OrthancPlugins::RegisterStorageCommitmentScpCallback(const void* parameters) + { + const _OrthancPluginRegisterStorageCommitmentScpCallback& p = + *reinterpret_cast<const _OrthancPluginRegisterStorageCommitmentScpCallback*>(parameters); + + boost::mutex::scoped_lock lock(pimpl_->storageCommitmentScpMutex_); + LOG(INFO) << "Plugin has registered a storage commitment callback"; + + pimpl_->storageCommitmentScpCallbacks_.push_back(new PImpl::StorageCommitmentScp(p)); + } + + void OrthancPlugins::AnswerBuffer(const void* parameters) { const _OrthancPluginAnswerBuffer& p = @@ -3882,6 +4009,10 @@ RegisterRefreshMetricsCallback(parameters); return true; + case _OrthancPluginService_RegisterStorageCommitmentScpCallback: + RegisterStorageCommitmentScpCallback(parameters); + return true; + case _OrthancPluginService_RegisterStorageArea: { LOG(INFO) << "Plugin has registered a custom storage area"; @@ -4539,4 +4670,32 @@ } } } + + + IStorageCommitmentFactory::ILookupHandler* OrthancPlugins::CreateStorageCommitment( + const std::string& jobId, + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::string& remoteAet, + const std::string& calledAet) + { + boost::mutex::scoped_lock lock(pimpl_->storageCommitmentScpMutex_); + + for (PImpl::StorageCommitmentScpCallbacks::iterator + it = pimpl_->storageCommitmentScpCallbacks_.begin(); + it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it) + { + assert(*it != NULL); + IStorageCommitmentFactory::ILookupHandler* handler = (*it)->CreateStorageCommitment + (jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet); + + if (handler != NULL) + { + return handler; + } + } + + return NULL; + } }
--- a/Plugins/Engine/OrthancPlugins.h Fri Feb 14 17:22:28 2020 +0100 +++ b/Plugins/Engine/OrthancPlugins.h Fri Feb 14 17:55:53 2020 +0100 @@ -62,6 +62,7 @@ #include "../../Core/JobsEngine/IJob.h" #include "../../OrthancServer/IDicomImageDecoder.h" #include "../../OrthancServer/IServerListener.h" +#include "../../OrthancServer/ServerJobs/IStorageCommitmentFactory.h" #include "OrthancPluginDatabase.h" #include "PluginsManager.h" @@ -80,7 +81,8 @@ public IDicomImageDecoder, public IIncomingHttpRequestFilter, public IFindRequestHandlerFactory, - public IMoveRequestHandlerFactory + public IMoveRequestHandlerFactory, + public IStorageCommitmentFactory { private: class PImpl; @@ -124,6 +126,8 @@ void RegisterRefreshMetricsCallback(const void* parameters); + void RegisterStorageCommitmentScpCallback(const void* parameters); + void AnswerBuffer(const void* parameters); void Redirect(const void* parameters); @@ -341,6 +345,15 @@ HttpMethod method, const UriComponents& uri, const Arguments& headers); + + // New in Orthanc 1.6.0 + IStorageCommitmentFactory::ILookupHandler* CreateStorageCommitment( + const std::string& jobId, + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::string& remoteAet, + const std::string& calledAet) ORTHANC_OVERRIDE; }; }
--- a/Plugins/Engine/PluginsEnumerations.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Plugins/Engine/PluginsEnumerations.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -549,5 +549,36 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason) + { + switch (reason) + { + case OrthancPluginStorageCommitmentFailureReason_Success: + return StorageCommitmentFailureReason_Success; + + case OrthancPluginStorageCommitmentFailureReason_ProcessingFailure: + return StorageCommitmentFailureReason_ProcessingFailure; + + case OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance: + return StorageCommitmentFailureReason_NoSuchObjectInstance; + + case OrthancPluginStorageCommitmentFailureReason_ResourceLimitation: + return StorageCommitmentFailureReason_ResourceLimitation; + + case OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported: + return StorageCommitmentFailureReason_ReferencedSOPClassNotSupported; + + case OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict: + return StorageCommitmentFailureReason_ClassInstanceConflict; + + case OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID: + return StorageCommitmentFailureReason_DuplicateTransactionUID; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } } }
--- a/Plugins/Engine/PluginsEnumerations.h Fri Feb 14 17:22:28 2020 +0100 +++ b/Plugins/Engine/PluginsEnumerations.h Fri Feb 14 17:55:53 2020 +0100 @@ -79,6 +79,8 @@ OrthancPluginJobStepStatus Convert(JobStepCode step); JobStepCode Convert(OrthancPluginJobStepStatus step); + + StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason); } }
--- a/Plugins/Engine/PluginsJob.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Plugins/Engine/PluginsJob.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -78,7 +78,7 @@ parameters_.finalize(parameters_.job); } - JobStepResult PluginsJob::Step() + JobStepResult PluginsJob::Step(const std::string& jobId) { OrthancPluginJobStepStatus status = parameters_.step(parameters_.job);
--- a/Plugins/Engine/PluginsJob.h Fri Feb 14 17:22:28 2020 +0100 +++ b/Plugins/Engine/PluginsJob.h Fri Feb 14 17:55:53 2020 +0100 @@ -55,7 +55,7 @@ { } - virtual JobStepResult Step(); + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; virtual void Reset();
--- a/Plugins/Include/orthanc/OrthancCPlugin.h Fri Feb 14 17:22:28 2020 +0100 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Fri Feb 14 17:55:53 2020 +0100 @@ -26,6 +26,7 @@ * - Possibly register a callback to unserialize jobs using OrthancPluginRegisterJobsUnserializer(). * - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback(). * - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback(). + * - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback(). * -# <tt>void OrthancPluginFinalize()</tt>: * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -122,16 +123,16 @@ #endif #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 -#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 5 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 7 +#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 6 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 0 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) -#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \ - (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major || \ - (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major && \ - (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor || \ - (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor && \ +#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \ + (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major || \ + (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major && \ + (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor || \ + (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor && \ ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision)))) #endif @@ -301,6 +302,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 @@ -450,6 +452,7 @@ _OrthancPluginService_RegisterIncomingHttpRequestFilter2 = 1010, _OrthancPluginService_RegisterRefreshMetricsCallback = 1011, _OrthancPluginService_RegisterChunkedRestCallback = 1012, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013, /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -909,14 +912,14 @@ **/ typedef enum { - OrthancPluginMetricsType_Default, /*!< Default metrics */ + OrthancPluginMetricsType_Default = 0, /*!< Default metrics */ /** * This metrics represents a time duration. Orthanc will keep the * maximum value of the metrics over a sliding window of ten * seconds, which is useful if the metrics is sampled frequently. **/ - OrthancPluginMetricsType_Timer + OrthancPluginMetricsType_Timer = 1 } OrthancPluginMetricsType; @@ -926,11 +929,47 @@ **/ typedef enum { - OrthancPluginDicomWebBinaryMode_Ignore, /*!< Don't include binary tags */ - OrthancPluginDicomWebBinaryMode_InlineBinary, /*!< Inline encoding using Base64 */ - OrthancPluginDicomWebBinaryMode_BulkDataUri /*!< Use a bulk data URI field */ + OrthancPluginDicomWebBinaryMode_Ignore = 0, /*!< Don't include binary tags */ + OrthancPluginDicomWebBinaryMode_InlineBinary = 1, /*!< Inline encoding using Base64 */ + OrthancPluginDicomWebBinaryMode_BulkDataUri = 2 /*!< Use a bulk data URI field */ } OrthancPluginDicomWebBinaryMode; + + /** + * The available values for the Failure Reason (0008,1197) during + * storage commitment. + * http://dicom.nema.org/medical/dicom/2019e/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 + **/ + typedef enum + { + OrthancPluginStorageCommitmentFailureReason_Success = 0, + + /* 0110H: A general failure in processing the operation was + * encountered */ + OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1, + + /* 0112H: One or more of the elements in the Referenced SOP + Instance Sequence was not available */ + OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2, + + /* 0213H: The SCP does not currently have enough resources to + store the requested SOP Instance(s) */ + OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3, + + /* 0122H: Storage Commitment has been requested for a SOP Instance + with a SOP Class that is not supported by the SCP */ + OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4, + + /* 0119H: The SOP Class of an element in the Referenced SOP + Instance Sequence did not correspond to the SOP class + registered for this SOP Instance at the SCP */ + OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict = 5, + + /* 0131H: The Transaction UID of the Storage Commitment Request is + already in use */ + OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID = 6 + } OrthancPluginStorageCommitmentFailureReason; + /** @@ -1658,7 +1697,8 @@ sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) || sizeof(int32_t) != sizeof(OrthancPluginConstraintType) || sizeof(int32_t) != sizeof(OrthancPluginMetricsType) || - sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode)) + sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || + sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason)) { /* Mismatch in the size of the enumerations */ return 0; @@ -7259,6 +7299,45 @@ } + + typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentFactory) ( + void** handler /* out */, + const char* jobId, + const char* transactionUid, + const char* const* sopClassUids, + const char* const* sopInstanceUids, + uint32_t countInstances, + const char* remoteAet, + const char* calledAet); + + typedef void (*OrthancPluginStorageCommitmentDestructor) (void* handler); + + typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentLookup) ( + OrthancPluginStorageCommitmentFailureReason* target, + void* handler, + const char* sopClassUid, + const char* sopInstanceUid); + + + typedef struct + { + OrthancPluginStorageCommitmentFactory factory; + OrthancPluginStorageCommitmentDestructor destructor; + OrthancPluginStorageCommitmentLookup lookup; + } _OrthancPluginRegisterStorageCommitmentScpCallback; + + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageCommitmentScpCallback( + OrthancPluginContext* context, + OrthancPluginStorageCommitmentFactory factory, + OrthancPluginStorageCommitmentDestructor destructor, + OrthancPluginStorageCommitmentLookup lookup) + { + _OrthancPluginRegisterStorageCommitmentScpCallback params; + params.factory = factory; + params.destructor = destructor; + params.lookup = lookup; + context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, ¶ms); + } #ifdef __cplusplus }
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri Feb 14 17:22:28 2020 +0100 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -3139,4 +3139,41 @@ } #endif } + + +#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1 + OrthancPluginErrorCode IStorageCommitmentScpHandler::Lookup( + OrthancPluginStorageCommitmentFailureReason* target, + void* rawHandler, + const char* sopClassUid, + const char* sopInstanceUid) + { + assert(target != NULL && + rawHandler != NULL); + + try + { + IStorageCommitmentScpHandler& handler = *reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler); + *target = handler.Lookup(sopClassUid, sopInstanceUid); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1 + void IStorageCommitmentScpHandler::Destructor(void* rawHandler) + { + assert(rawHandler != NULL); + delete reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler); + } +#endif }
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h Fri Feb 14 17:22:28 2020 +0100 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h Fri Feb 14 17:55:53 2020 +0100 @@ -103,6 +103,12 @@ # define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER 0 #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 0) +# define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP 1 +#else +# define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP 0 +#endif + namespace OrthancPlugins @@ -1100,4 +1106,26 @@ #endif } }; + + + +#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1 + class IStorageCommitmentScpHandler : public boost::noncopyable + { + public: + virtual ~IStorageCommitmentScpHandler() + { + } + + virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid, + const std::string& sopInstanceUid) = 0; + + static OrthancPluginErrorCode Lookup(OrthancPluginStorageCommitmentFailureReason* target, + void* rawHandler, + const char* sopClassUid, + const char* sopInstanceUid); + + static void Destructor(void* rawHandler); + }; +#endif }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt Fri Feb 14 17:55:53 2020 +0100 @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 2.8) + +project(StorageCommitmentScp) + +SET(PLUGIN_VERSION "0.0" CACHE STRING "Version of the plugin") +SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") +SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") + +SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") +SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") + +set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) +include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) + +add_library(StorageCommitmentScp SHARED + Plugin.cpp + ../Common/OrthancPluginCppWrapper.cpp + ${JSONCPP_SOURCES} + ${BOOST_SOURCES} + ) + +message("Setting the version of the plugin to ${PLUGIN_VERSION}") +add_definitions( + -DPLUGIN_VERSION="${PLUGIN_VERSION}" + ) + +set_target_properties(StorageCommitmentScp PROPERTIES + VERSION ${PLUGIN_VERSION} + SOVERSION ${PLUGIN_VERSION}) + +install( + TARGETS StorageCommitmentScp + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/StorageCommitmentScp/Plugin.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -0,0 +1,113 @@ +/** + * 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. + * + * 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 "../Common/OrthancPluginCppWrapper.h" + +#include <json/value.h> +#include <json/reader.h> + + + +class StorageCommitmentSample : public OrthancPlugins::IStorageCommitmentScpHandler +{ +private: + int count_; + +public: + StorageCommitmentSample() : count_(0) + { + } + + virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid, + const std::string& sopInstanceUid) + { + printf("?? [%s] [%s]\n", sopClassUid.c_str(), sopInstanceUid.c_str()); + if (count_++ % 2 == 0) + return OrthancPluginStorageCommitmentFailureReason_Success; + else + return OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance; + } +}; + + +static OrthancPluginErrorCode StorageCommitmentScp(void** handler /* out */, + const char* jobId, + const char* transactionUid, + const char* const* sopClassUids, + const char* const* sopInstanceUids, + uint32_t countInstances, + const char* remoteAet, + const char* calledAet) +{ + printf("[%s] [%s] [%s] [%s]\n", jobId, transactionUid, remoteAet, calledAet); + + for (uint32_t i = 0; i < countInstances; i++) + { + printf("++ [%s] [%s]\n", sopClassUids[i], sopInstanceUids[i]); + } + + *handler = new StorageCommitmentSample; + return OrthancPluginErrorCode_Success; +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + OrthancPlugins::SetGlobalContext(c); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 0) + { + OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + return -1; + } + + OrthancPluginSetDescription(c, "Sample storage commitment SCP plugin."); + + OrthancPluginRegisterStorageCommitmentScpCallback( + c, StorageCommitmentScp, + OrthancPlugins::IStorageCommitmentScpHandler::Destructor, + OrthancPlugins::IStorageCommitmentScpHandler::Lookup); + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "storage-commitment-scp"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return PLUGIN_VERSION; + } +}
--- a/Resources/Configuration.json Fri Feb 14 17:22:28 2020 +0100 +++ b/Resources/Configuration.json Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/Resources/ErrorCodes.json Fri Feb 14 17:55:53 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 Fri Feb 14 17:22:28 2020 +0100 +++ b/UnitTestsSources/MultiThreadingTests.cpp Fri Feb 14 17:55:53 2020 +0100 @@ -101,7 +101,7 @@ { } - virtual JobStepResult Step() ORTHANC_OVERRIDE + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE { if (fails_) { @@ -1045,12 +1045,12 @@ job.AddInstance("nope"); job.AddInstance("world"); job.SetPermissive(true); - ASSERT_THROW(job.Step(), OrthancException); // Not started yet + ASSERT_THROW(job.Step("jobId"), OrthancException); // Not started yet ASSERT_FALSE(job.HasTrailingStep()); ASSERT_FALSE(job.IsTrailingStepDone()); job.Start(); - ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); - ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); { DummyUnserializer unserializer; @@ -1101,7 +1101,7 @@ lock.SetTrailingOperationTimeout(300); } - ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); { GenericJobUnserializer unserializer; @@ -1618,8 +1618,8 @@ job.AddTrailingStep(); job.Start(); - ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); - ASSERT_EQ(JobStepCode_Success, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); study2 = job.GetTargetStudy(); ASSERT_FALSE(study2.empty()); @@ -1677,8 +1677,8 @@ job.AddTrailingStep(); job.Start(); - ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); - ASSERT_EQ(JobStepCode_Success, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); ASSERT_TRUE(job.Serialize(s)); @@ -1746,7 +1746,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_EQ(JobStepCode_Success, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); ASSERT_EQ(1u, job.GetPosition()); ASSERT_FALSE(job.IsTrailingStepDone()); @@ -1755,7 +1755,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_THROW(job.Step(), OrthancException); + ASSERT_THROW(job.Step("jobId"), OrthancException); } { @@ -1777,7 +1777,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); ASSERT_EQ(1u, job.GetPosition()); ASSERT_FALSE(job.IsTrailingStepDone()); @@ -1786,7 +1786,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_EQ(JobStepCode_Success, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); ASSERT_EQ(2u, job.GetPosition()); ASSERT_FALSE(job.IsTrailingStepDone()); @@ -1795,7 +1795,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_THROW(job.Step(), OrthancException); + ASSERT_THROW(job.Step("jobId"), OrthancException); } { @@ -1818,7 +1818,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_EQ(JobStepCode_Success, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); ASSERT_EQ(1u, job.GetPosition()); ASSERT_TRUE(job.IsTrailingStepDone()); @@ -1827,7 +1827,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_THROW(job.Step(), OrthancException); + ASSERT_THROW(job.Step("jobId"), OrthancException); } { @@ -1852,7 +1852,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); ASSERT_EQ(1u, job.GetPosition()); ASSERT_FALSE(job.IsTrailingStepDone()); @@ -1861,7 +1861,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_EQ(JobStepCode_Success, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode()); ASSERT_EQ(2u, job.GetPosition()); ASSERT_TRUE(job.IsTrailingStepDone()); @@ -1870,7 +1870,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_THROW(job.Step(), OrthancException); + ASSERT_THROW(job.Step("jobId"), OrthancException); } } @@ -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 Fri Feb 14 17:22:28 2020 +0100 +++ b/UnitTestsSources/ToolboxTests.cpp Fri Feb 14 17:55:53 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)); +}