Mercurial > hg > orthanc
changeset 3799:320a2d224902
merge
author | Alain Mazy <alain@mazy.be> |
---|---|
date | Wed, 01 Apr 2020 10:15:33 +0200 |
parents | c38b82bb6fd3 (current diff) d73ce7c537c3 (diff) |
children | 38b0f51781aa |
files | Core/DicomNetworking/DicomUserConnection.cpp |
diffstat | 91 files changed, 4826 insertions(+), 527 deletions(-) [+] |
line wrap: on
line diff
--- a/AUTHORS Wed Apr 01 10:14:49 2020 +0200 +++ b/AUTHORS Wed Apr 01 10:15:33 2020 +0200 @@ -15,7 +15,7 @@ Belgium * Osimis S.A. - Rue du Bois Saint-Jean 15/1 - 4102 Seraing + Quai Banning 6 + 4000 Liege Belgium http://www.osimis.io/
--- a/CMakeLists.txt Wed Apr 01 10:14:49 2020 +0200 +++ b/CMakeLists.txt Wed Apr 01 10:15:33 2020 +0200 @@ -25,6 +25,9 @@ set(ENABLE_WEB_SERVER ON) set(ENABLE_ZLIB ON) +# To test transcoding +#set(ENABLE_DCMTK_TRANSCODING ON) + set(HAS_EMBEDDED_RESOURCES ON) @@ -103,8 +106,10 @@ OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp OrthancServer/ServerJobs/ResourceModificationJob.cpp OrthancServer/ServerJobs/SplitStudyJob.cpp + OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp OrthancServer/ServerToolbox.cpp OrthancServer/SliceOrdering.cpp + OrthancServer/StorageCommitmentReports.cpp ) @@ -483,9 +488,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/Compatibility.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Compatibility.h Wed Apr 01 10:15:33 2020 +0200 @@ -47,17 +47,16 @@ // the compiler ("/Zc:__cplusplus") // To make this header more robust, we use the _MSVC_LANG equivalent macro. -# if _MSC_VER > 1900 -# if (defined _MSVC_LANG) && (_MSVC_LANG >= 201103L) -# define ORTHANC_Cxx03_DETECTED 0 -# else -# define ORTHANC_Cxx03_DETECTED 1 -# endif -# elif _MSC_VER > 1800 +// please note that not all C++11 features are supported when _MSC_VER == 1600 +// (or higher). This header file can be made for fine-grained, if required, +// based on specific _MSC_VER values + +# if _MSC_VER >= 1600 # define ORTHANC_Cxx03_DETECTED 0 # else # define ORTHANC_Cxx03_DETECTED 1 # endif + #else // of _MSC_VER is not defined, we assume __cplusplus is correctly defined // if __cplusplus is not defined (very old compilers??), then the following @@ -89,12 +88,12 @@ class unique_ptr : public boost::movelib::unique_ptr<T> { public: - unique_ptr() : + explicit unique_ptr() : boost::movelib::unique_ptr<T>() { } - unique_ptr(T* p) : + explicit unique_ptr(T* p) : boost::movelib::unique_ptr<T>(p) { }
--- a/Core/DicomFormat/DicomMap.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomFormat/DicomMap.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -745,9 +745,8 @@ } - bool DicomMap::ParseDicomMetaInformation(DicomMap& result, - const char* dicom, - size_t size) + bool DicomMap::IsDicomFile(const char* dicom, + size_t size) { /** * http://dicom.nema.org/medical/dicom/current/output/chtml/part10/chapter_7.html @@ -756,11 +755,19 @@ * account to determine whether the file is or is not a DICOM file. **/ - if (size < 132 || - dicom[128] != 'D' || - dicom[129] != 'I' || - dicom[130] != 'C' || - dicom[131] != 'M') + return (size >= 132 && + dicom[128] == 'D' && + dicom[129] == 'I' && + dicom[130] == 'C' && + dicom[131] == 'M'); + } + + + bool DicomMap::ParseDicomMetaInformation(DicomMap& result, + const char* dicom, + size_t size) + { + if (!IsDicomFile(dicom, size)) { return false; }
--- a/Core/DicomFormat/DicomMap.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomFormat/DicomMap.h Wed Apr 01 10:15:33 2020 +0200 @@ -180,6 +180,9 @@ void GetTags(std::set<DicomTag>& tags) const; + static bool IsDicomFile(const char* dicom, + size_t size); + static bool ParseDicomMetaInformation(DicomMap& result, const char* dicom, size_t size);
--- a/Core/DicomNetworking/DicomServer.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/DicomServer.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/DicomServer.h Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -86,6 +86,7 @@ # error The macro DCMTK_VERSION_NUMBER must be defined #endif +#include "../Compatibility.h" #include "../DicomFormat/DicomArray.h" #include "../Logging.h" #include "../OrthancException.h" @@ -158,7 +159,9 @@ void CheckIsOpen() const; - void Store(DcmInputStream& is, + void Store(std::string& sopClassUidOut /* out */, + std::string& sopInstanceUidOut /* out */, + DcmInputStream& is, DicomUserConnection& connection, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); @@ -252,7 +255,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 +278,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) { @@ -307,7 +354,9 @@ } - void DicomUserConnection::PImpl::Store(DcmInputStream& is, + void DicomUserConnection::PImpl::Store(std::string& sopClassUidOut, + std::string& sopInstanceUidOut, + DcmInputStream& is, DicomUserConnection& connection, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) @@ -390,6 +439,9 @@ connection.remoteAet_); } + sopClassUidOut.assign(sopClass); + sopInstanceUidOut.assign(sopInstance); + // Figure out which of the accepted presentation contexts should be used int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass); if (presID == 0) @@ -1060,7 +1112,7 @@ } } - void DicomUserConnection::Open() + void DicomUserConnection::OpenInternal(Mode mode) { if (IsOpen()) { @@ -1100,7 +1152,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_), @@ -1144,7 +1196,9 @@ return pimpl_->IsOpen(); } - void DicomUserConnection::Store(const char* buffer, + void DicomUserConnection::Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const char* buffer, size_t size, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) @@ -1155,26 +1209,31 @@ is.setBuffer(buffer, size); is.setEos(); - pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); + pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); } - void DicomUserConnection::Store(const std::string& buffer, + void DicomUserConnection::Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& buffer, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) { if (buffer.size() > 0) - Store(&buffer[0], buffer.size(), moveOriginatorAET, moveOriginatorID); + Store(sopClassUid, sopInstanceUid, &buffer[0], buffer.size(), + moveOriginatorAET, moveOriginatorID); else - Store(NULL, 0, moveOriginatorAET, moveOriginatorID); + Store(sopClassUid, sopInstanceUid, NULL, 0, moveOriginatorAET, moveOriginatorID); } - void DicomUserConnection::StoreFile(const std::string& path, + void DicomUserConnection::StoreFile(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& path, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) { // Prepare an input stream for the file DcmInputFileStream is(path.c_str()); - pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); + pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); } bool DicomUserConnection::Echo() @@ -1405,4 +1464,369 @@ 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::unique_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; + + const int timeout = pimpl_->dimseTimeout_; + if (!DIMSE_receiveCommand(pimpl_->assoc_, + (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout, + &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); + } + + for (size_t i = 0; i < sopClassUids.size(); i++) + { + if (sopClassUids[i].empty() || + sopInstanceUids[i].empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "The SOP class/instance UIDs cannot be empty, found: \"" + + sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\""); + } + } + + 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; + + const int timeout = pimpl_->dimseTimeout_; + if (!DIMSE_receiveCommand(pimpl_->assoc_, + (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout, + &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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/DicomUserConnection.h Wed Apr 01 10:15:33 2020 +0200 @@ -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(); @@ -145,33 +158,45 @@ bool Echo(); - void Store(const char* buffer, + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const char* buffer, size_t size, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); - void Store(const char* buffer, + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const char* buffer, size_t size) { - Store(buffer, size, "", 0); // Not a C-Move + Store(sopClassUid, sopInstanceUid, buffer, size, "", 0); // Not a C-Move } - void Store(const std::string& buffer, + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& buffer, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); - void Store(const std::string& buffer) + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& buffer) { - Store(buffer, "", 0); // Not a C-Move + Store(sopClassUid, sopInstanceUid, buffer, "", 0); // Not a C-Move } - void StoreFile(const std::string& path, + void StoreFile(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& path, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); - void StoreFile(const std::string& path) + void StoreFile(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& path) { - StoreFile(path, "", 0); // Not a C-Move + StoreFile(sopClassUid, sopInstanceUid, path, "", 0); // Not a C-Move } void Find(DicomFindAnswers& result, @@ -212,5 +237,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 Wed Apr 01 10:15:33 2020 +0200 @@ -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 <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::vector<StorageCommitmentFailureReason>& failureReasons, + 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 Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -93,9 +93,12 @@ #include "../../Compatibility.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> @@ -272,33 +275,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, @@ -362,146 +338,206 @@ << " 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); + } -#if DCMTK_VERSION_NUMBER >= 361 - // 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); - } -#endif - - 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); + } + +#if DCMTK_VERSION_NUMBER >= 361 + // 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); + } +#endif + + 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 */ @@ -703,6 +739,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; @@ -784,6 +830,14 @@ } break; + case DicomRequestType_NAction: + cond = NActionScp(&msg, presID); + break; + + case DicomRequestType_NEventReport: + cond = NEventReportScp(&msg, presID); + break; + default: // Should never happen break; @@ -837,5 +891,379 @@ } return cond; } + + + static DcmDataset* ReadDataset(T_ASC_Association* assoc, + const char* errorMessage, + int timeout) + { + DcmDataset *tmp = NULL; + T_ASC_PresentationContextID presIdData; + + OFCondition cond = DIMSE_receiveDataSetInMemory( + assoc, (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout, + &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, + std::vector<StorageCommitmentFailureReason>* failureReasons, // Can be NULL + DcmDataset& dataset, + const DcmTagKey& tag, + bool mandatory) + { + sopClassUids.clear(); + sopInstanceUids.clear(); + + if (failureReasons) + { + failureReasons->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()); + + if (failureReasons) + { + failureReasons->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 dataset"); + } + + sopClassUids.push_back(a); + sopInstanceUids.push_back(b); + + if (failureReasons != NULL) + { + Uint16 reason; + if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good()) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Missing Failure Reason (0008,1197) " + "in storage commitment dataset"); + } + + failureReasons->push_back(static_cast<StorageCommitmentFailureReason>(reason)); + } + } + } + + + 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::unique_ptr<DcmDataset> dataset( + ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP", associationTimeout_)); + + std::string transactionUid = ReadString(*dataset, DCM_TransactionUID); + + std::vector<std::string> sopClassUid, sopInstanceUid; + ReadSopSequence(sopClassUid, sopInstanceUid, NULL, + *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::unique_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::unique_ptr<DcmDataset> dataset( + ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP", associationTimeout_)); + + std::string transactionUid = ReadString(*dataset, DCM_TransactionUID); + + std::vector<std::string> successSopClassUid, successSopInstanceUid; + ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL, + *dataset, DCM_ReferencedSOPSequence, + (report.EventTypeID == 1) /* mandatory in the case of success */); + + std::vector<std::string> failedSopClassUid, failedSopInstanceUid; + std::vector<StorageCommitmentFailureReason> failureReasons; + + if (report.EventTypeID == 2 /* failures exist */) + { + ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons, + *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::unique_ptr<IStorageCommitmentRequestHandler> handler + (server_.GetStorageCommitmentRequestHandlerFactory(). + ConstructStorageCommitmentRequestHandler()); + + handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid, + failedSopClassUid, failedSopInstanceUid, failureReasons, + 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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.h Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.h Wed Apr 01 10:15:33 2020 +0200 @@ -53,7 +53,9 @@ bool allowFind_; bool allowMove_; bool allowGet_; - + bool allowNAction_; + bool allowNEventReport_; + void Clear(); void UnserializeArray(const Json::Value& serialized);
--- a/Core/DicomParsing/FromDcmtkBridge.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -68,10 +68,11 @@ #include <dcmtk/dcmdata/dcdicent.h> #include <dcmtk/dcmdata/dcdict.h> #include <dcmtk/dcmdata/dcfilefo.h> +#include <dcmtk/dcmdata/dcistrmb.h> #include <dcmtk/dcmdata/dcostrmb.h> #include <dcmtk/dcmdata/dcpixel.h> #include <dcmtk/dcmdata/dcuid.h> -#include <dcmtk/dcmdata/dcistrmb.h> +#include <dcmtk/dcmdata/dcxfer.h> #include <dcmtk/dcmdata/dcvrae.h> #include <dcmtk/dcmdata/dcvras.h> @@ -1878,7 +1879,15 @@ std::unique_ptr<DcmFileFormat> result(new DcmFileFormat); result->transferInit(); - if (!result->read(is).good()) + + /** + * New in Orthanc 1.6.0: The "size" is given as an argument to the + * "read()" method. This can avoid huge memory consumption if + * parsing an invalid DICOM file, which can notably been observed + * by executing the integration test "test_upload_compressed" on + * valgrind running Orthanc. + **/ + if (!result->read(is, EXS_Unknown, EGL_noChange, size).good()) { throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse an invalid DICOM file (size: " + @@ -2081,7 +2090,7 @@ // Unregister JPEG codecs DJDecoderRegistration::cleanup(); # if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 - DJDecoderRegistration::cleanup(); + DJEncoderRegistration::cleanup(); # endif #endif }
--- a/Core/DicomParsing/FromDcmtkBridge.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.h Wed Apr 01 10:15:33 2020 +0200 @@ -271,7 +271,10 @@ ITagVisitor& visitor, Encoding defaultEncoding); - static bool GetDcmtkTransferSyntax(E_TransferSyntax& target, - DicomTransferSyntax syntax); + static bool LookupDcmtkTransferSyntax(E_TransferSyntax& target, + DicomTransferSyntax source); + + static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target, + E_TransferSyntax source); }; }
--- a/Core/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h Wed Apr 01 10:15:33 2020 +0200 @@ -34,10 +34,10 @@ namespace Orthanc { - bool GetDcmtkTransferSyntax(E_TransferSyntax& target, - DicomTransferSyntax syntax) + bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target, + DicomTransferSyntax source) { - switch (syntax) + switch (source) { case DicomTransferSyntax_LittleEndianImplicit: target = EXS_LittleEndianImplicit; @@ -56,75 +56,147 @@ return true; case DicomTransferSyntax_JPEGProcess1: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess1TransferSyntax; +# else target = EXS_JPEGProcess1; +# endif return true; case DicomTransferSyntax_JPEGProcess2_4: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess2_4TransferSyntax; +# else target = EXS_JPEGProcess2_4; +# endif return true; case DicomTransferSyntax_JPEGProcess3_5: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess3_5TransferSyntax; +# else target = EXS_JPEGProcess3_5; +# endif return true; case DicomTransferSyntax_JPEGProcess6_8: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess6_8TransferSyntax; +# else target = EXS_JPEGProcess6_8; +# endif return true; case DicomTransferSyntax_JPEGProcess7_9: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess7_9TransferSyntax; +# else target = EXS_JPEGProcess7_9; +# endif return true; case DicomTransferSyntax_JPEGProcess10_12: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess10_12TransferSyntax; +# else target = EXS_JPEGProcess10_12; +# endif return true; case DicomTransferSyntax_JPEGProcess11_13: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess11_13TransferSyntax; +# else target = EXS_JPEGProcess11_13; +# endif return true; case DicomTransferSyntax_JPEGProcess14: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess14TransferSyntax; +# else target = EXS_JPEGProcess14; +# endif return true; case DicomTransferSyntax_JPEGProcess15: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess15TransferSyntax; +# else target = EXS_JPEGProcess15; +# endif return true; case DicomTransferSyntax_JPEGProcess16_18: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess16_18TransferSyntax; +# else target = EXS_JPEGProcess16_18; +# endif return true; case DicomTransferSyntax_JPEGProcess17_19: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess17_19TransferSyntax; +# else target = EXS_JPEGProcess17_19; +# endif return true; case DicomTransferSyntax_JPEGProcess20_22: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess20_22TransferSyntax; +# else target = EXS_JPEGProcess20_22; +# endif return true; case DicomTransferSyntax_JPEGProcess21_23: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess21_23TransferSyntax; +# else target = EXS_JPEGProcess21_23; +# endif return true; case DicomTransferSyntax_JPEGProcess24_26: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess24_26TransferSyntax; +# else target = EXS_JPEGProcess24_26; +# endif return true; case DicomTransferSyntax_JPEGProcess25_27: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess25_27TransferSyntax; +# else target = EXS_JPEGProcess25_27; +# endif return true; case DicomTransferSyntax_JPEGProcess28: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess28TransferSyntax; +# else target = EXS_JPEGProcess28; +# endif return true; case DicomTransferSyntax_JPEGProcess29: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess29TransferSyntax; +# else target = EXS_JPEGProcess29; +# endif return true; case DicomTransferSyntax_JPEGProcess14SV1: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess14SV1TransferSyntax; +# else target = EXS_JPEGProcess14SV1; +# endif return true; case DicomTransferSyntax_JPEGLSLossless: @@ -167,25 +239,35 @@ target = EXS_MPEG2MainProfileAtHighLevel; return true; +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4HighProfileLevel4_1: target = EXS_MPEG4HighProfileLevel4_1; return true; +#endif +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1: target = EXS_MPEG4BDcompatibleHighProfileLevel4_1; return true; +#endif +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo: target = EXS_MPEG4HighProfileLevel4_2_For2DVideo; return true; +#endif +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo: target = EXS_MPEG4HighProfileLevel4_2_For3DVideo; return true; +#endif +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2: target = EXS_MPEG4StereoHighProfileLevel4_2; return true; +#endif #if DCMTK_VERSION_NUMBER >= 362 case DicomTransferSyntax_HEVCMainProfileLevel5_1: @@ -207,4 +289,261 @@ return false; } } + + + bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target, + E_TransferSyntax source) + { + switch (source) + { + case EXS_LittleEndianImplicit: + target = DicomTransferSyntax_LittleEndianImplicit; + return true; + + case EXS_LittleEndianExplicit: + target = DicomTransferSyntax_LittleEndianExplicit; + return true; + + case EXS_DeflatedLittleEndianExplicit: + target = DicomTransferSyntax_DeflatedLittleEndianExplicit; + return true; + + case EXS_BigEndianExplicit: + target = DicomTransferSyntax_BigEndianExplicit; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess1TransferSyntax: +# else + case EXS_JPEGProcess1: +# endif + target = DicomTransferSyntax_JPEGProcess1; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess2_4TransferSyntax: +# else + case EXS_JPEGProcess2_4: +# endif + target = DicomTransferSyntax_JPEGProcess2_4; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess3_5TransferSyntax: +# else + case EXS_JPEGProcess3_5: +# endif + target = DicomTransferSyntax_JPEGProcess3_5; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess6_8TransferSyntax: +# else + case EXS_JPEGProcess6_8: +# endif + target = DicomTransferSyntax_JPEGProcess6_8; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess7_9TransferSyntax: +# else + case EXS_JPEGProcess7_9: +# endif + target = DicomTransferSyntax_JPEGProcess7_9; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess10_12TransferSyntax: +# else + case EXS_JPEGProcess10_12: +# endif + target = DicomTransferSyntax_JPEGProcess10_12; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess11_13TransferSyntax: +# else + case EXS_JPEGProcess11_13: +# endif + target = DicomTransferSyntax_JPEGProcess11_13; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess14TransferSyntax: +# else + case EXS_JPEGProcess14: +# endif + target = DicomTransferSyntax_JPEGProcess14; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess15TransferSyntax: +# else + case EXS_JPEGProcess15: +# endif + target = DicomTransferSyntax_JPEGProcess15; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess16_18TransferSyntax: +# else + case EXS_JPEGProcess16_18: +# endif + target = DicomTransferSyntax_JPEGProcess16_18; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess17_19TransferSyntax: +# else + case EXS_JPEGProcess17_19: +# endif + target = DicomTransferSyntax_JPEGProcess17_19; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess20_22TransferSyntax: +# else + case EXS_JPEGProcess20_22: +# endif + target = DicomTransferSyntax_JPEGProcess20_22; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess21_23TransferSyntax: +# else + case EXS_JPEGProcess21_23: +# endif + target = DicomTransferSyntax_JPEGProcess21_23; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess24_26TransferSyntax: +# else + case EXS_JPEGProcess24_26: +# endif + target = DicomTransferSyntax_JPEGProcess24_26; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess25_27TransferSyntax: +# else + case EXS_JPEGProcess25_27: +# endif + target = DicomTransferSyntax_JPEGProcess25_27; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess28TransferSyntax: +# else + case EXS_JPEGProcess28: +# endif + target = DicomTransferSyntax_JPEGProcess28; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess29TransferSyntax: +# else + case EXS_JPEGProcess29: +# endif + target = DicomTransferSyntax_JPEGProcess29; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess14SV1TransferSyntax: +# else + case EXS_JPEGProcess14SV1: +# endif + target = DicomTransferSyntax_JPEGProcess14SV1; + return true; + + case EXS_JPEGLSLossless: + target = DicomTransferSyntax_JPEGLSLossless; + return true; + + case EXS_JPEGLSLossy: + target = DicomTransferSyntax_JPEGLSLossy; + return true; + + case EXS_JPEG2000LosslessOnly: + target = DicomTransferSyntax_JPEG2000LosslessOnly; + return true; + + case EXS_JPEG2000: + target = DicomTransferSyntax_JPEG2000; + return true; + + case EXS_JPEG2000MulticomponentLosslessOnly: + target = DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly; + return true; + + case EXS_JPEG2000Multicomponent: + target = DicomTransferSyntax_JPEG2000Multicomponent; + return true; + + case EXS_JPIPReferenced: + target = DicomTransferSyntax_JPIPReferenced; + return true; + + case EXS_JPIPReferencedDeflate: + target = DicomTransferSyntax_JPIPReferencedDeflate; + return true; + + case EXS_MPEG2MainProfileAtMainLevel: + target = DicomTransferSyntax_MPEG2MainProfileAtMainLevel; + return true; + + case EXS_MPEG2MainProfileAtHighLevel: + target = DicomTransferSyntax_MPEG2MainProfileAtHighLevel; + return true; + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4HighProfileLevel4_1: + target = DicomTransferSyntax_MPEG4HighProfileLevel4_1; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4BDcompatibleHighProfileLevel4_1: + target = DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4HighProfileLevel4_2_For2DVideo: + target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4HighProfileLevel4_2_For3DVideo: + target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4StereoHighProfileLevel4_2: + target = DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 362 + case EXS_HEVCMainProfileLevel5_1: + target = DicomTransferSyntax_HEVCMainProfileLevel5_1; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 362 + case EXS_HEVCMain10ProfileLevel5_1: + target = DicomTransferSyntax_HEVCMain10ProfileLevel5_1; + return true; +#endif + + case EXS_RLELossless: + target = DicomTransferSyntax_RLELossless; + return true; + + default: + return false; + } + } }
--- a/Core/DicomParsing/Internals/DicomFrameIndex.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomFrameIndex.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -68,7 +68,10 @@ uint32_t length = item->getLength(); if (length == 0) { - table.clear(); + // Degenerate case: Empty offset table means only one frame + // that overlaps all the fragments + table.resize(1); + table[0] = 0; return; } @@ -146,7 +149,6 @@ throw OrthancException(ErrorCode_BadFileFormat); } - // Loop over the fragments (ignoring the offset table). This is // an alternative, faster implementation to DCMTK's // "DcmCodec::determineStartFragment()". @@ -318,46 +320,10 @@ }; - - bool DicomFrameIndex::IsVideo(DcmFileFormat& dicom) + unsigned int DicomFrameIndex::GetFramesCount(DcmDataset& dicom) { - // Retrieve the transfer syntax from the DICOM header - const char* value = NULL; - if (!dicom.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() || - value == NULL) - { - return false; - } - - const std::string transferSyntax(value); - - // Video standards supported in DICOM 2016a - // http://dicom.nema.org/medical/dicom/2016a/output/html/part05.html - if (transferSyntax == "1.2.840.10008.1.2.4.100" || // MPEG2 MP@ML option of ISO/IEC MPEG2 - transferSyntax == "1.2.840.10008.1.2.4.101" || // MPEG2 MP@HL option of ISO/IEC MPEG2 - transferSyntax == "1.2.840.10008.1.2.4.102" || // MPEG-4 AVC/H.264 High Profile / Level 4.1 of ITU-T H.264 - transferSyntax == "1.2.840.10008.1.2.4.103" || // MPEG-4 AVC/H.264 BD-compat High Profile / Level 4.1 of ITU-T H.264 - transferSyntax == "1.2.840.10008.1.2.4.104" || // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264 - transferSyntax == "1.2.840.10008.1.2.4.105" || // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264 - transferSyntax == "1.2.840.10008.1.2.4.106") // MPEG-4 AVC/H.264 Stereo High Profile / Level 4.2 of the ITU-T H.264 - { - return true; - } - - return false; - } - - - unsigned int DicomFrameIndex::GetFramesCount(DcmFileFormat& dicom) - { - // Assume 1 frame for video transfer syntaxes - if (IsVideo(dicom)) - { - return 1; - } - const char* tmp = NULL; - if (!dicom.getDataset()->findAndGetString(DCM_NumberOfFrames, tmp).good() || + if (!dicom.findAndGetString(DCM_NumberOfFrames, tmp).good() || tmp == NULL) { return 1; @@ -378,12 +344,12 @@ } else { - return count; + return static_cast<unsigned int>(count); } } - DicomFrameIndex::DicomFrameIndex(DcmFileFormat& dicom) + DicomFrameIndex::DicomFrameIndex(DcmDataset& dicom) { countFrames_ = GetFramesCount(dicom); if (countFrames_ == 0) @@ -392,10 +358,8 @@ return; } - DcmDataset& dataset = *dicom.getDataset(); - // Test whether this image is composed of a sequence of fragments - DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset); + DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dicom); if (pixelSequence != NULL) { index_.reset(new FragmentIndex(pixelSequence, countFrames_)); @@ -404,18 +368,18 @@ // Extract information about the image structure DicomMap tags; - FromDcmtkBridge::ExtractDicomSummary(tags, dataset); + FromDcmtkBridge::ExtractDicomSummary(tags, dicom); DicomImageInformation information(tags); // Access to the raw pixel data - if (DicomImageDecoder::IsPsmctRle1(dataset)) + if (DicomImageDecoder::IsPsmctRle1(dicom)) { - index_.reset(new PsmctRle1Index(dataset, countFrames_, information.GetFrameSize())); + index_.reset(new PsmctRle1Index(dicom, countFrames_, information.GetFrameSize())); } else { - index_.reset(new UncompressedIndex(dataset, countFrames_, information.GetFrameSize())); + index_.reset(new UncompressedIndex(dicom, countFrames_, information.GetFrameSize())); } }
--- a/Core/DicomParsing/Internals/DicomFrameIndex.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomFrameIndex.h Wed Apr 01 10:15:33 2020 +0200 @@ -67,7 +67,7 @@ unsigned int countFrames_; public: - DicomFrameIndex(DcmFileFormat& dicom); + DicomFrameIndex(DcmDataset& dicom); unsigned int GetFramesCount() const { @@ -77,8 +77,6 @@ void GetRawFrame(std::string& frame, unsigned int index) const; - static bool IsVideo(DcmFileFormat& dicom); - - static unsigned int GetFramesCount(DcmFileFormat& dicom); + static unsigned int GetFramesCount(DcmDataset& dicom); }; }
--- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -34,6 +34,8 @@ #include "../../PrecompiledHeaders.h" #include "DicomImageDecoder.h" +#include "../ParsedDicomFile.h" + /*========================================================================= @@ -84,7 +86,6 @@ #include "../../DicomFormat/DicomIntegerPixelAccessor.h" #include "../ToDcmtkBridge.h" #include "../FromDcmtkBridge.h" -#include "../ParsedDicomFile.h" #if ORTHANC_ENABLE_PNG == 1 # include "../../Images/PngWriter.h" @@ -98,7 +99,6 @@ #include <boost/lexical_cast.hpp> #include <dcmtk/dcmdata/dcdeftag.h> -#include <dcmtk/dcmdata/dcfilefo.h> #include <dcmtk/dcmdata/dcrleccd.h> #include <dcmtk/dcmdata/dcrlecp.h> #include <dcmtk/dcmdata/dcrlerp.h> @@ -662,7 +662,20 @@ ImageAccessor* DicomImageDecoder::Decode(ParsedDicomFile& dicom, unsigned int frame) { - DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + if (dicom.GetDcmtkObject().getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + return Decode(*dicom.GetDcmtkObject().getDataset(), frame); + } + } + + + ImageAccessor* DicomImageDecoder::Decode(DcmDataset& dataset, + unsigned int frame) + { E_TransferSyntax syntax = dataset.getOriginalXfer(); /**
--- a/Core/DicomParsing/Internals/DicomImageDecoder.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.h Wed Apr 01 10:15:33 2020 +0200 @@ -34,7 +34,7 @@ #pragma once #include "../../Compatibility.h" -#include "../ParsedDicomFile.h" +#include "../../Images/ImageAccessor.h" #include <memory> @@ -62,6 +62,8 @@ namespace Orthanc { + class ParsedDicomFile; + class DicomImageDecoder : public boost::noncopyable { private: @@ -102,6 +104,9 @@ static ImageAccessor *Decode(ParsedDicomFile& dicom, unsigned int frame); + static ImageAccessor *Decode(DcmDataset& dataset, + unsigned int frame); + static void ExtractPamImage(std::string& result, std::unique_ptr<ImageAccessor>& image, ImageExtractionMode mode,
--- a/Core/DicomParsing/ParsedDicomFile.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -1557,7 +1557,9 @@ { if (pimpl_->frameIndex_.get() == NULL) { - pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_)); + assert(pimpl_->file_ != NULL && + pimpl_->file_->getDataset() != NULL); + pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_->getDataset())); } pimpl_->frameIndex_->GetRawFrame(target, frameId); @@ -1589,7 +1591,9 @@ unsigned int ParsedDicomFile::GetFramesCount() const { - return DicomFrameIndex::GetFramesCount(*pimpl_->file_); + assert(pimpl_->file_ != NULL && + pimpl_->file_->getDataset() != NULL); + return DicomFrameIndex::GetFramesCount(*pimpl_->file_->getDataset()); }
--- a/Core/Enumerations.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Enumerations.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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); } @@ -1157,6 +1168,41 @@ } + const char* EnumerationToString(StorageCommitmentFailureReason reason) + { + switch (reason) + { + case StorageCommitmentFailureReason_Success: + return "Success"; + + case StorageCommitmentFailureReason_ProcessingFailure: + return "A general failure in processing the operation was encountered"; + + case StorageCommitmentFailureReason_NoSuchObjectInstance: + return "One or more of the elements in the Referenced SOP " + "Instance Sequence was not available"; + + case StorageCommitmentFailureReason_ResourceLimitation: + return "The SCP does not currently have enough resources to " + "store the requested SOP Instance(s)"; + + case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported: + return "Storage Commitment has been requested for a SOP Instance " + "with a SOP Class that is not supported by the SCP"; + + case StorageCommitmentFailureReason_ClassInstanceConflict: + return "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"; + + case StorageCommitmentFailureReason_DuplicateTransactionUID: + return "The Transaction UID of the Storage Commitment Request is already in use"; + + default: + return "Unknown failure reason"; + } + } + + Encoding StringToEncoding(const char* encoding) { std::string s(encoding);
--- a/Core/Enumerations.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Enumerations.h Wed Apr 01 10:15:33 2020 +0200 @@ -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 }; @@ -670,7 +671,9 @@ DicomRequestType_Find, DicomRequestType_Get, DicomRequestType_Move, - DicomRequestType_Store + DicomRequestType_Store, + DicomRequestType_NAction, + DicomRequestType_NEventReport }; enum TransferSyntax @@ -712,6 +715,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 @@ -798,6 +831,8 @@ const char* EnumerationToString(Endianness endianness); + const char* EnumerationToString(StorageCommitmentFailureReason reason); + Encoding StringToEncoding(const char* encoding); ResourceType StringToResourceType(const char* type);
--- a/Core/HttpClient.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/HttpClient.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/IJob.h Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/JobsEngine.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -71,7 +71,7 @@ try { - result = running.GetJob().Step(); + result = running.GetJob().Step(running.GetId()); } catch (OrthancException& e) {
--- a/Core/JobsEngine/JobsEngine.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/JobsEngine.h Wed Apr 01 10:15:33 2020 +0200 @@ -41,7 +41,7 @@ namespace Orthanc { - class JobsEngine + class JobsEngine : public boost::noncopyable { private: enum State
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Wed Apr 01 10:15:33 2020 +0200 @@ -126,30 +126,30 @@ size_t output); }; - virtual void Start() + virtual void Start() ORTHANC_OVERRIDE { } - virtual JobStepResult Step(); + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; - virtual void Reset(); + virtual void Reset() ORTHANC_OVERRIDE; - virtual void Stop(JobStopReason reason); + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - virtual float GetProgress(); + virtual float GetProgress() ORTHANC_OVERRIDE; - virtual void GetJobType(std::string& target) + virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE { target = "SequenceOfOperations"; } - virtual void GetPublicContent(Json::Value& value); + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& value); + virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE; virtual bool GetOutput(std::string& output, MimeType& mime, - const std::string& key) + const std::string& key) ORTHANC_OVERRIDE { return false; }
--- a/Core/JobsEngine/SetOfCommandsJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/SetOfCommandsJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/SetOfCommandsJob.h Wed Apr 01 10:15:33 2020 +0200 @@ -49,7 +49,7 @@ { } - virtual bool Execute() = 0; + virtual bool Execute(const std::string& jobId) = 0; virtual void Serialize(Json::Value& target) const = 0; }; @@ -110,14 +110,14 @@ void SetPermissive(bool permissive); - virtual void Reset(); + virtual void Reset() ORTHANC_OVERRIDE; - virtual void Start() + virtual void Start() ORTHANC_OVERRIDE { started_ = true; } - virtual float GetProgress(); + virtual float GetProgress() ORTHANC_OVERRIDE; bool IsStarted() const { @@ -126,15 +126,15 @@ 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); + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target); + virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; virtual bool GetOutput(std::string& output, MimeType& mime, - const std::string& key) + const std::string& key) ORTHANC_OVERRIDE { return false; }
--- a/Core/JobsEngine/SetOfInstancesJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/SetOfInstancesJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -60,7 +60,7 @@ return instance_; } - virtual bool Execute() + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE { if (!that_.HandleInstance(instance_)) { @@ -73,7 +73,7 @@ } } - virtual void Serialize(Json::Value& target) const + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE { target = 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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/SerializationToolbox.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/SerializationToolbox.h Wed Apr 01 10:15:33 2020 +0200 @@ -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/SharedLibrary.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/SharedLibrary.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -62,14 +62,19 @@ } #elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) - + /** * "RTLD_LOCAL" is the default, and is only present to be * explicit. "RTLD_DEEPBIND" was added in Orthanc 1.6.0, in order * to avoid crashes while loading plugins from the LSB binaries of * the Orthanc core. + * + * BUT this had no effect, and this results in a crash if loading + * the Python 2.7 plugin => We disabled it again in Orthanc 1.6.1. **/ -#if defined(RTLD_DEEPBIND) // This is a GNU extension + +#if 0 // && defined(RTLD_DEEPBIND) // This is a GNU extension + // Disabled in Orthanc 1.6.1 handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND); #else handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL);
--- a/Core/Toolbox.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Toolbox.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -2078,6 +2078,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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Toolbox.h Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/NEWS Wed Apr 01 10:15:33 2020 +0200 @@ -1,24 +1,47 @@ Pending changes in the mainline =============================== + +Maintenance +----------- + +* Source code repository moved from BitBucket to self-hosted server +* Fix lookup form in Orthanc Explorer (wildcards not allowed in StudyDate) +* Fix signature of "OrthancPluginRegisterStorageCommitmentScpCallback()" in plugins SDK + + +Version 1.6.0 (2020-03-18) +========================== + +General +------- + +* Support of DICOM storage commitment + REST API -------- * API version has been upgraded to 5 -* added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually - retrieve its version number) -* "/changes": Allow the "limit" argument to be greater than 100 -* "/instances/{id}/preview": Now takes the windowing into account -* "/tools/log-level": Possibility to access and change the log level without restarting Orthanc -* added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes - to render frames, taking windowing and resizing into account -* "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files -* ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags +* Added: + - "/peers/{id}/system": Test the connectivity with a remote peer + (and also retrieve its version number) + - "/tools/log-level": Access and/or change the log level without restarting Orthanc + - "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered": + Render frames, taking windowing and resizing into account + - "/modalities/{...}/storage-commitment": Trigger storage commitment SCU + - "/storage-commitment/{...}": Access storage commitment reports + - "/storage-commitment/{...}/remove": Remove instances from storage commitment reports +* Improved: + - "/changes": Allow the "limit" argument to be greater than 100 + - "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files + - ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags + - "/modalities/{...}/store": New Boolean argument "StorageCommitment" Plugins ------- * New sample plugin: "ConnectivityChecks" +* New primitives to handle storage commitment SCP by plugins Lua --- @@ -32,6 +55,7 @@ Maintenance ----------- +* New configuration options: "DefaultPrivateCreator" and "StorageCommitmentReportsSize" * Support of MPEG4 transfer syntaxes in C-Store SCP * C-FIND SCU at Instance level now sets the 0008,0052 tag to IMAGE per default (was INSTANCE). Therefore, the "ClearCanvas" and "Dcm4Chee" modality manufacturer have now been deprecated.
--- a/OrthancExplorer/explorer.js Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancExplorer/explorer.js Wed Apr 01 10:15:33 2020 +0200 @@ -431,7 +431,7 @@ // NB: "GenerateDicomDate()" is defined in "query-retrieve.js" var target = $('#lookup-study-date'); $('option', target).remove(); - target.append($('<option>').attr('value', '*').text('Any date')); + target.append($('<option>').attr('value', '').text('Any date')); target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today')); target.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday')); target.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
--- a/OrthancExplorer/query-retrieve.js Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancExplorer/query-retrieve.js Wed Apr 01 10:15:33 2020 +0200 @@ -38,7 +38,7 @@ targetDate = $('#qr-date'); $('option', targetDate).remove(); - targetDate.append($('<option>').attr('value', '*').text('Any date')); + targetDate.append($('<option>').attr('value', '').text('Any date')); targetDate.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today')); targetDate.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday')); targetDate.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days')); @@ -90,7 +90,7 @@ 'PatientName' : '', 'PatientSex' : '', 'StudyDate' : $('#qr-date').val(), - 'StudyDescription' : '*' + 'StudyDescription' : '' } }; @@ -204,10 +204,10 @@ query = { 'Level' : 'Series', 'Query' : { - 'Modality' : '*', - 'ProtocolName' : '*', - 'SeriesDescription' : '*', - 'SeriesInstanceUID' : '*', + 'Modality' : '', + 'ProtocolName' : '', + 'SeriesDescription' : '', + 'SeriesInstanceUID' : '', 'StudyInstanceUID' : pageData.uuid } };
--- a/OrthancServer/OrthancMoveRequestHandler.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -116,7 +116,8 @@ connection_.reset(new DicomUserConnection(localAet_, remote_)); } - connection_->Store(dicom, originatorAet_, originatorId_); + std::string sopClassUid, sopInstanceUid; // Unused + connection_->Store(sopClassUid, sopInstanceUid, dicom, originatorAet_, originatorId_); return Status_Success; }
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -46,6 +46,7 @@ #include "../ServerJobs/DicomMoveScuJob.h" #include "../ServerJobs/OrthancPeerStoreJob.h" #include "../ServerToolbox.h" +#include "../StorageCommitmentReports.h" namespace Orthanc @@ -963,6 +964,12 @@ job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID); } + // New in Orthanc 1.6.0 + if (Toolbox::GetJsonBooleanField(request, "StorageCommitment", false)) + { + job->EnableStorageCommitment(true); + } + OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, request); } @@ -1273,7 +1280,7 @@ if (call.ParseJsonRequest(json)) { const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = + const RemoteModalityParameters remote = MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); std::unique_ptr<ParsedDicomFile> query @@ -1299,6 +1306,251 @@ } + // Storage commitment SCU --------------------------------------------------- + + static void StorageCommitmentScu(RestApiPostCall& call) + { + static const char* const ORTHANC_RESOURCES = "Resources"; + static const char* const DICOM_INSTANCES = "DicomInstances"; + static const char* const SOP_CLASS_UID = "SOPClassUID"; + static const char* const SOP_INSTANCE_UID = "SOPInstanceUID"; + + ServerContext& context = OrthancRestApi::GetContext(call); + + Json::Value json; + if (!call.ParseJsonRequest(json) || + json.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Must provide a JSON object with a list of resources"); + } + else if (!json.isMember(ORTHANC_RESOURCES) && + !json.isMember(DICOM_INSTANCES)) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Empty storage commitment request, one of these fields is mandatory: \"" + + std::string(ORTHANC_RESOURCES) + "\" or \"" + std::string(DICOM_INSTANCES) + "\""); + } + else + { + std::list<std::string> sopClassUids, sopInstanceUids; + + if (json.isMember(ORTHANC_RESOURCES)) + { + const Json::Value& resources = json[ORTHANC_RESOURCES]; + + if (resources.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "The \"" + std::string(ORTHANC_RESOURCES) + + "\" field must provide an array of Orthanc resources"); + } + else + { + for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++) + { + if (resources[i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "The \"" + std::string(ORTHANC_RESOURCES) + + "\" field must provide an array of strings, found: " + resources[i].toStyledString()); + } + + std::list<std::string> instances; + context.GetIndex().GetChildInstances(instances, resources[i].asString()); + + for (std::list<std::string>::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + std::string sopClassUid, sopInstanceUid; + DicomMap tags; + if (context.LookupOrReconstructMetadata(sopClassUid, *it, MetadataType_Instance_SopClassUid) && + context.GetIndex().GetAllMainDicomTags(tags, *it) && + tags.LookupStringValue(sopInstanceUid, DICOM_TAG_SOP_INSTANCE_UID, false)) + { + sopClassUids.push_back(sopClassUid); + sopInstanceUids.push_back(sopInstanceUid); + } + else + { + throw OrthancException(ErrorCode_InternalError, + "Cannot retrieve SOP Class/Instance UID of Orthanc instance: " + *it); + } + } + } + } + } + + if (json.isMember(DICOM_INSTANCES)) + { + const Json::Value& instances = json[DICOM_INSTANCES]; + + if (instances.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "The \"" + std::string(DICOM_INSTANCES) + + "\" field must provide an array of DICOM instances"); + } + else + { + for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++) + { + if (instances[i].type() == Json::arrayValue) + { + if (instances[i].size() != 2 || + instances[i][0].type() != Json::stringValue || + instances[i][1].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "An instance entry must provide an array with 2 strings: " + "SOP Class UID and SOP Instance UID"); + } + else + { + sopClassUids.push_back(instances[i][0].asString()); + sopInstanceUids.push_back(instances[i][1].asString()); + } + } + else if (instances[i].type() == Json::objectValue) + { + if (!instances[i].isMember(SOP_CLASS_UID) || + !instances[i].isMember(SOP_INSTANCE_UID) || + instances[i][SOP_CLASS_UID].type() != Json::stringValue || + instances[i][SOP_INSTANCE_UID].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "An instance entry must provide an object with 2 string fiels: " + "\"" + std::string(SOP_CLASS_UID) + "\" and \"" + + std::string(SOP_INSTANCE_UID)); + } + else + { + sopClassUids.push_back(instances[i][SOP_CLASS_UID].asString()); + sopInstanceUids.push_back(instances[i][SOP_INSTANCE_UID].asString()); + } + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "JSON array or object is expected to specify one " + "instance to be queried, found: " + instances[i].toStyledString()); + } + } + } + } + + if (sopClassUids.size() != sopInstanceUids.size()) + { + throw OrthancException(ErrorCode_InternalError); + } + + const std::string transactionUid = Toolbox::GenerateDicomPrivateUniqueIdentifier(); + + if (sopClassUids.empty()) + { + LOG(WARNING) << "Issuing an outgoing storage commitment request that is empty: " << transactionUid; + } + + { + const RemoteModalityParameters remote = + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + const std::string& remoteAet = remote.GetApplicationEntityTitle(); + const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); + + // Create a "pending" storage commitment report BEFORE the + // actual SCU call in order to avoid race conditions + context.GetStorageCommitmentReports().Store( + transactionUid, new StorageCommitmentReports::Report(remoteAet)); + + DicomUserConnection scu(localAet, remote); + + std::vector<std::string> a(sopClassUids.begin(), sopClassUids.end()); + std::vector<std::string> b(sopInstanceUids.begin(), sopInstanceUids.end()); + scu.RequestStorageCommitment(transactionUid, a, b); + } + + Json::Value result = Json::objectValue; + result["ID"] = transactionUid; + result["Path"] = "/storage-commitment/" + transactionUid; + call.GetOutput().AnswerJson(result); + } + } + + + static void GetStorageCommitmentReport(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + const std::string& transactionUid = call.GetUriComponent("id", ""); + + { + StorageCommitmentReports::Accessor accessor( + context.GetStorageCommitmentReports(), transactionUid); + + if (accessor.IsValid()) + { + Json::Value json; + accessor.GetReport().Format(json); + call.GetOutput().AnswerJson(json); + } + else + { + throw OrthancException(ErrorCode_InexistentItem, + "No storage commitment transaction with UID: " + transactionUid); + } + } + } + + + static void RemoveAfterStorageCommitment(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + const std::string& transactionUid = call.GetUriComponent("id", ""); + + { + StorageCommitmentReports::Accessor accessor( + context.GetStorageCommitmentReports(), transactionUid); + + if (!accessor.IsValid()) + { + throw OrthancException(ErrorCode_InexistentItem, + "No storage commitment transaction with UID: " + transactionUid); + } + else if (accessor.GetReport().GetStatus() != StorageCommitmentReports::Report::Status_Success) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Cannot remove DICOM instances after failure " + "in storage commitment transaction: " + transactionUid); + } + else + { + std::vector<std::string> sopInstanceUids; + accessor.GetReport().GetSuccessSopInstanceUids(sopInstanceUids); + + for (size_t i = 0; i < sopInstanceUids.size(); i++) + { + std::vector<std::string> orthancId; + context.GetIndex().LookupIdentifierExact( + orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids[i]); + + for (size_t j = 0; j < orthancId.size(); j++) + { + LOG(INFO) << "Storage commitment - Removing SOP instance UID / Orthanc ID: " + << sopInstanceUids[i] << " / " << orthancId[j]; + + Json::Value tmp; + context.GetIndex().DeleteResource(tmp, orthancId[j], ResourceType_Instance); + } + } + + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + } + } + } + + void OrthancRestApi::RegisterModalities() { Register("/modalities", ListModalities); @@ -1342,5 +1594,10 @@ Register("/peers/{id}/system", PeerSystem); Register("/modalities/{id}/find-worklist", DicomFindWorklist); + + // Storage commitment + Register("/modalities/{id}/storage-commitment", StorageCommitmentScu); + Register("/storage-commitment/{id}", GetStorageCommitmentReport); + Register("/storage-commitment/{id}/remove", RemoveAfterStorageCommitment); } }
--- a/OrthancServer/Search/DicomTagConstraint.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/Search/DicomTagConstraint.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -105,7 +105,8 @@ (value.find('*') != std::string::npos || value.find('?') != std::string::npos)) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Wildcards are not allowed on tag " + tag_.Format()); } if (constraintType_ == ConstraintType_Equal ||
--- a/OrthancServer/ServerContext.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/ServerContext.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -49,6 +49,7 @@ #include "Search/DatabaseLookup.h" #include "ServerJobs/OrthancJobUnserializer.h" #include "ServerToolbox.h" +#include "StorageCommitmentReports.h" #include <EmbeddedResources.h> #include <dcmtk/dcmdata/dcfilefo.h> @@ -259,6 +260,9 @@ findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always")); limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0); limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0); + + // New configuration option in Orthanc 1.6.0 + storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100))); } jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); @@ -1062,4 +1066,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 Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/ServerContext.h Wed Apr 01 10:15:33 2020 +0200 @@ -37,6 +37,7 @@ #include "LuaScripting.h" #include "OrthancHttpHandler.h" #include "ServerIndex.h" +#include "ServerJobs/IStorageCommitmentFactory.h" #include "../Core/Cache/MemoryCache.h" @@ -53,6 +54,7 @@ class SetOfInstancesJob; class SharedArchive; class SharedMessageQueue; + class StorageCommitmentReports; /** @@ -60,7 +62,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 @@ -164,11 +168,11 @@ void SaveJobsEngine(); - virtual void SignalJobSubmitted(const std::string& jobId); + virtual void SignalJobSubmitted(const std::string& jobId) ORTHANC_OVERRIDE; - virtual void SignalJobSuccess(const std::string& jobId); + virtual void SignalJobSuccess(const std::string& jobId) ORTHANC_OVERRIDE; - virtual void SignalJobFailure(const std::string& jobId); + virtual void SignalJobFailure(const std::string& jobId) ORTHANC_OVERRIDE; ServerIndex index_; IStorageArea& area_; @@ -218,6 +222,8 @@ bool isHttpServerSecure_; bool isExecuteLuaEnabled_; + std::unique_ptr<StorageCommitmentReports> storageCommitmentReports_; + public: class DicomCacheLocker : public boost::noncopyable { @@ -419,5 +425,18 @@ { 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; + + StorageCommitmentReports& GetStorageCommitmentReports() + { + return *storageCommitmentReports_; + } }; }
--- a/OrthancServer/ServerJobs/ArchiveJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/ServerJobs/ArchiveJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -935,7 +935,7 @@ } - JobStepResult ArchiveJob::Step() + JobStepResult ArchiveJob::Step(const std::string& jobId) { assert(writer_.get() != NULL);
--- a/OrthancServer/ServerJobs/ArchiveJob.h Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/ServerJobs/ArchiveJob.h Wed Apr 01 10:15:33 2020 +0200 @@ -89,29 +89,29 @@ void AddResource(const std::string& publicId); - virtual void Reset(); + virtual void Reset() ORTHANC_OVERRIDE; - virtual void Start(); + virtual void Start() ORTHANC_OVERRIDE; - virtual JobStepResult Step(); + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; - virtual void Stop(JobStopReason reason) + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE { } - virtual float GetProgress(); + virtual float GetProgress() ORTHANC_OVERRIDE; - virtual void GetJobType(std::string& target); + virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE; - virtual void GetPublicContent(Json::Value& value); + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& value) + virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE { return false; // Cannot serialize this kind of job } virtual bool GetOutput(std::string& output, MimeType& mime, - const std::string& key); + const std::string& key) ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -38,6 +38,7 @@ #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" +#include "../StorageCommitmentReports.h" namespace Orthanc @@ -72,14 +73,47 @@ LOG(WARNING) << "An instance was removed after the job was issued: " << instance; return false; } + + std::string sopClassUid, sopInstanceUid; if (HasMoveOriginator()) { - connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_); + connection_->Store(sopClassUid, sopInstanceUid, dicom, moveOriginatorAet_, moveOriginatorId_); } else { - connection_->Store(dicom); + connection_->Store(sopClassUid, sopInstanceUid, dicom); + } + + if (storageCommitment_) + { + sopClassUids_.push_back(sopClassUid); + sopInstanceUids_.push_back(sopInstanceUid); + + if (sopClassUids_.size() != sopInstanceUids_.size() || + sopClassUids_.size() > GetInstancesCount()) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (sopClassUids_.size() == GetInstancesCount()) + { + const std::string& remoteAet = remote_.GetApplicationEntityTitle(); + + LOG(INFO) << "Sending storage commitment request to modality: " << remoteAet; + + // Create a "pending" storage commitment report BEFORE the + // actual SCU call in order to avoid race conditions + context_.GetStorageCommitmentReports().Store( + transactionUid_, new StorageCommitmentReports::Report(remoteAet)); + + assert(IsStarted()); + OpenConnection(); + + std::vector<std::string> a(sopClassUids_.begin(), sopClassUids_.end()); + std::vector<std::string> b(sopInstanceUids_.begin(), sopInstanceUids_.end()); + connection_->RequestStorageCommitment(transactionUid_, a, b); + } } //boost::this_thread::sleep(boost::posix_time::milliseconds(500)); @@ -97,8 +131,10 @@ DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) : context_(context), localAet_("ORTHANC"), - moveOriginatorId_(0) // By default, not a C-MOVE + moveOriginatorId_(0), // By default, not a C-MOVE + storageCommitment_(false) // By default, no storage commitment { + ResetStorageCommitment(); } @@ -179,6 +215,38 @@ } + void DicomModalityStoreJob::ResetStorageCommitment() + { + if (storageCommitment_) + { + transactionUid_ = Toolbox::GenerateDicomPrivateUniqueIdentifier(); + sopClassUids_.clear(); + sopInstanceUids_.clear(); + } + } + + + void DicomModalityStoreJob::Reset() + { + SetOfInstancesJob::Reset(); + + /** + * "After the N-EVENT-REPORT has been sent, the Transaction UID is + * no longer active and shall not be reused for other + * transactions." => Need to reset the transaction UID here + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html + **/ + ResetStorageCommitment(); + } + + + void DicomModalityStoreJob::EnableStorageCommitment(bool enabled) + { + storageCommitment_ = enabled; + ResetStorageCommitment(); + } + + void DicomModalityStoreJob::GetPublicContent(Json::Value& value) { SetOfInstancesJob::GetPublicContent(value); @@ -191,6 +259,11 @@ value["MoveOriginatorAET"] = GetMoveOriginatorAet(); value["MoveOriginatorID"] = GetMoveOriginatorId(); } + + if (storageCommitment_) + { + value["StorageCommitmentTransactionUID"] = transactionUid_; + } } @@ -198,6 +271,7 @@ static const char* REMOTE = "Remote"; static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet"; static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId"; + static const char* STORAGE_COMMITMENT = "StorageCommitment"; DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context, @@ -210,6 +284,7 @@ moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET); moveOriginatorId_ = static_cast<uint16_t> (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID)); + EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT)); } @@ -225,6 +300,7 @@ remote_.Serialize(target[REMOTE], true /* force advanced format */); target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_; target[MOVE_ORIGINATOR_ID] = moveOriginatorId_; + target[STORAGE_COMMITMENT] = storageCommitment_; return true; } }
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h Wed Apr 01 10:15:33 2020 +0200 @@ -50,13 +50,21 @@ std::string moveOriginatorAet_; uint16_t moveOriginatorId_; std::unique_ptr<DicomUserConnection> connection_; + bool storageCommitment_; + + // For storage commitment + std::string transactionUid_; + std::list<std::string> sopInstanceUids_; + std::list<std::string> sopClassUids_; void OpenConnection(); + void ResetStorageCommitment(); + protected: - virtual bool HandleInstance(const std::string& instance); + virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE; - virtual bool HandleTrailingStep(); + virtual bool HandleTrailingStep() ORTHANC_OVERRIDE; public: DicomModalityStoreJob(ServerContext& context); @@ -90,15 +98,19 @@ void SetMoveOriginator(const std::string& aet, int id); - virtual void Stop(JobStopReason reason); + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - virtual void GetJobType(std::string& target) + virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE { target = "DicomModalityStore"; } - virtual void GetPublicContent(Json::Value& value); + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + + virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target); + virtual void Reset() ORTHANC_OVERRIDE; + + void EnableStorageCommitment(bool enabled); }; }
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -57,13 +57,13 @@ { } - virtual bool Execute() + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE { that_.Retrieve(*findAnswer_); return true; } - virtual void Serialize(Json::Value& target) const + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE { findAnswer_->Serialize(target); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/IStorageCommitmentFactory.h Wed Apr 01 10:15:33 2020 +0200 @@ -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/Operations/StoreScuOperation.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -70,7 +70,9 @@ { std::string dicom; instance.ReadDicom(dicom); - resource->GetConnection().Store(dicom); + + std::string sopClassUid, sopInstanceUid; // Unused + resource->GetConnection().Store(sopClassUid, sopInstanceUid, dicom); } catch (OrthancException& e) {
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:15:33 2020 +0200 @@ -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 ORTHANC_OVERRIDE + { + return CommandType_Setup; + } + + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE + { + that_.Setup(jobId); + return true; + } + + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE + { + 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 ORTHANC_OVERRIDE + { + 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 ORTHANC_OVERRIDE + { + 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 ORTHANC_OVERRIDE + { + return CommandType_Answer; + } + + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE + { + that_.Answer(); + return true; + } + + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE + { + 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 Wed Apr 01 10:15:33 2020 +0200 @@ -0,0 +1,111 @@ +/** + * 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/Compatibility.h" +#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::unique_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; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/StorageCommitmentReports.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -0,0 +1,272 @@ +/** + * 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 "StorageCommitmentReports.h" + +#include "../Core/OrthancException.h" + +namespace Orthanc +{ + void StorageCommitmentReports::Report::MarkAsComplete() + { + if (isComplete_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + isComplete_ = true; + } + } + + void StorageCommitmentReports::Report::AddSuccess(const std::string& sopClassUid, + const std::string& sopInstanceUid) + { + if (isComplete_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Success success; + success.sopClassUid_ = sopClassUid; + success.sopInstanceUid_ = sopInstanceUid; + success_.push_back(success); + } + } + + void StorageCommitmentReports::Report::AddFailure(const std::string& sopClassUid, + const std::string& sopInstanceUid, + StorageCommitmentFailureReason reason) + { + if (isComplete_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Failure failure; + failure.sopClassUid_ = sopClassUid; + failure.sopInstanceUid_ = sopInstanceUid; + failure.reason_ = reason; + failures_.push_back(failure); + } + } + + + StorageCommitmentReports::Report::Status StorageCommitmentReports::Report::GetStatus() const + { + if (!isComplete_) + { + return Status_Pending; + } + else if (failures_.empty()) + { + return Status_Success; + } + else + { + return Status_Failure; + } + } + + + void StorageCommitmentReports::Report::Format(Json::Value& json) const + { + static const char* const FIELD_STATUS = "Status"; + static const char* const FIELD_SOP_CLASS_UID = "SOPClassUID"; + static const char* const FIELD_SOP_INSTANCE_UID = "SOPInstanceUID"; + static const char* const FIELD_FAILURE_REASON = "FailureReason"; + static const char* const FIELD_DESCRIPTION = "Description"; + static const char* const FIELD_REMOTE_AET = "RemoteAET"; + static const char* const FIELD_SUCCESS = "Success"; + static const char* const FIELD_FAILURES = "Failures"; + + + json = Json::objectValue; + json[FIELD_REMOTE_AET] = remoteAet_; + + bool pending; + + switch (GetStatus()) + { + case Status_Pending: + json[FIELD_STATUS] = "Pending"; + pending = true; + break; + + case Status_Success: + json[FIELD_STATUS] = "Success"; + pending = false; + break; + + case Status_Failure: + json[FIELD_STATUS] = "Failure"; + pending = false; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (!pending) + { + { + Json::Value success = Json::arrayValue; + for (std::list<Success>::const_iterator + it = success_.begin(); it != success_.end(); ++it) + { + Json::Value item = Json::objectValue; + item[FIELD_SOP_CLASS_UID] = it->sopClassUid_; + item[FIELD_SOP_INSTANCE_UID] = it->sopInstanceUid_; + success.append(item); + } + + json[FIELD_SUCCESS] = success; + } + + { + Json::Value failures = Json::arrayValue; + for (std::list<Failure>::const_iterator + it = failures_.begin(); it != failures_.end(); ++it) + { + Json::Value item = Json::objectValue; + item[FIELD_SOP_CLASS_UID] = it->sopClassUid_; + item[FIELD_SOP_INSTANCE_UID] = it->sopInstanceUid_; + item[FIELD_FAILURE_REASON] = it->reason_; + item[FIELD_DESCRIPTION] = EnumerationToString(it->reason_); + failures.append(item); + } + + json[FIELD_FAILURES] = failures; + } + } + } + + + void StorageCommitmentReports::Report::GetSuccessSopInstanceUids( + std::vector<std::string>& target) const + { + target.clear(); + target.reserve(success_.size()); + + for (std::list<Success>::const_iterator + it = success_.begin(); it != success_.end(); ++it) + { + target.push_back(it->sopInstanceUid_); + } + } + + + StorageCommitmentReports::~StorageCommitmentReports() + { + while (!content_.IsEmpty()) + { + Report* report = NULL; + content_.RemoveOldest(report); + + assert(report != NULL); + delete report; + } + } + + + void StorageCommitmentReports::Store(const std::string& transactionUid, + Report* report) + { + std::unique_ptr<Report> protection(report); + + boost::mutex::scoped_lock lock(mutex_); + + { + Report* previous = NULL; + if (content_.Contains(transactionUid, previous)) + { + assert(previous != NULL); + delete previous; + + content_.Invalidate(transactionUid); + } + } + + assert(maxSize_ == 0 || + content_.GetSize() <= maxSize_); + + if (maxSize_ != 0 && + content_.GetSize() == maxSize_) + { + assert(!content_.IsEmpty()); + + Report* oldest = NULL; + content_.RemoveOldest(oldest); + + assert(oldest != NULL); + delete oldest; + } + + assert(maxSize_ == 0 || + content_.GetSize() < maxSize_); + + content_.Add(transactionUid, protection.release()); + } + + + StorageCommitmentReports::Accessor::Accessor(StorageCommitmentReports& that, + const std::string& transactionUid) : + lock_(that.mutex_), + transactionUid_(transactionUid) + { + if (that.content_.Contains(transactionUid, report_)) + { + that.content_.MakeMostRecent(transactionUid); + } + else + { + report_ = NULL; + } + } + + const StorageCommitmentReports::Report& + StorageCommitmentReports::Accessor::GetReport() const + { + if (report_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *report_; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/StorageCommitmentReports.h Wed Apr 01 10:15:33 2020 +0200 @@ -0,0 +1,147 @@ +/** + * 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/Cache/LeastRecentlyUsedIndex.h" + +namespace Orthanc +{ + class StorageCommitmentReports + { + public: + class Report : public boost::noncopyable + { + public: + enum Status + { + Status_Success, + Status_Failure, + Status_Pending + }; + + private: + struct Success + { + std::string sopClassUid_; + std::string sopInstanceUid_; + }; + + struct Failure + { + std::string sopClassUid_; + std::string sopInstanceUid_; + StorageCommitmentFailureReason reason_; + }; + + bool isComplete_; + std::list<Success> success_; + std::list<Failure> failures_; + std::string remoteAet_; + + public: + Report(const std::string& remoteAet) : + isComplete_(false), + remoteAet_(remoteAet) + { + } + + const std::string& GetRemoteAet() const + { + return remoteAet_; + } + + void MarkAsComplete(); + + void AddSuccess(const std::string& sopClassUid, + const std::string& sopInstanceUid); + + void AddFailure(const std::string& sopClassUid, + const std::string& sopInstanceUid, + StorageCommitmentFailureReason reason); + + Status GetStatus() const; + + void Format(Json::Value& json) const; + + void GetSuccessSopInstanceUids(std::vector<std::string>& target) const; + }; + + private: + typedef LeastRecentlyUsedIndex<std::string, Report*> Content; + + boost::mutex mutex_; + Content content_; + size_t maxSize_; + + public: + StorageCommitmentReports(size_t maxSize) : + maxSize_(maxSize) + { + } + + ~StorageCommitmentReports(); + + size_t GetMaxSize() const + { + return maxSize_; + } + + void Store(const std::string& transactionUid, + Report* report); // Takes ownership + + class Accessor : public boost::noncopyable + { + private: + boost::mutex::scoped_lock lock_; + std::string transactionUid_; + Report *report_; + + public: + Accessor(StorageCommitmentReports& that, + const std::string& transactionUid); + + const std::string& GetTransactionUid() const + { + return transactionUid_; + } + + bool IsValid() const + { + return report_ != NULL; + } + + const Report& GetReport() const; + }; + }; +}
--- a/OrthancServer/main.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancServer/main.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -36,6 +36,7 @@ #include <boost/algorithm/string/predicate.hpp> +#include "../Core/Compatibility.h" #include "../Core/DicomFormat/DicomArray.h" #include "../Core/DicomNetworking/DicomServer.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" @@ -50,7 +51,9 @@ #include "OrthancInitialization.h" #include "OrthancMoveRequestHandler.h" #include "ServerContext.h" +#include "ServerJobs/StorageCommitmentScpJob.h" #include "ServerToolbox.h" +#include "StorageCommitmentReports.h" using namespace Orthanc; @@ -58,11 +61,11 @@ class OrthancStoreRequestHandler : public IStoreRequestHandler { private: - ServerContext& server_; + ServerContext& context_; public: OrthancStoreRequestHandler(ServerContext& context) : - server_(context) + context_(context) { } @@ -84,8 +87,82 @@ 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::unique_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::vector<StorageCommitmentFailureReason>& failureReasons, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + if (successSopClassUids.size() != successSopInstanceUids.size() || + failedSopClassUids.size() != failedSopInstanceUids.size() || + failedSopClassUids.size() != failureReasons.size()) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::unique_ptr<StorageCommitmentReports::Report> report( + new StorageCommitmentReports::Report(remoteAet)); + + for (size_t i = 0; i < successSopClassUids.size(); i++) + { + report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]); + } + + for (size_t i = 0; i < failedSopClassUids.size(); i++) + { + report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]); + } + + report->MarkAsComplete(); + + context_.GetStorageCommitmentReports().Store(transactionUid, report.release()); } }; @@ -113,7 +190,8 @@ class MyDicomServerFactory : public IStoreRequestHandlerFactory, public IFindRequestHandlerFactory, - public IMoveRequestHandlerFactory + public IMoveRequestHandlerFactory, + public IStorageCommitmentRequestHandlerFactory { private: ServerContext& context_; @@ -166,6 +244,11 @@ return new OrthancMoveRequestHandler(context_); } + virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() + { + return new OrthancStorageCommitmentRequestHandler(context_); + } + void Done() { } @@ -676,6 +759,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 +1054,7 @@ dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory); dicomServer.SetFindRequestHandlerFactory(serverFactory); + dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory); { OrthancConfiguration::ReaderLock lock;
--- a/Plugins/Engine/OrthancPlugins.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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::unique_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_; @@ -1261,6 +1368,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) || @@ -1304,6 +1412,13 @@ { delete *it; } + + for (PImpl::StorageCommitmentScpCallbacks::iterator + it = pimpl_->storageCommitmentScpCallbacks_.begin(); + it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it) + { + delete *it; + } } @@ -1864,6 +1979,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 = @@ -3911,6 +4038,10 @@ RegisterRefreshMetricsCallback(parameters); return true; + case _OrthancPluginService_RegisterStorageCommitmentScpCallback: + RegisterStorageCommitmentScpCallback(parameters); + return true; + case _OrthancPluginService_RegisterStorageArea: { LOG(INFO) << "Plugin has registered a custom storage area"; @@ -4568,4 +4699,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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Engine/OrthancPlugins.h Wed Apr 01 10:15:33 2020 +0200 @@ -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); @@ -235,20 +239,20 @@ const Arguments& headers, const GetArguments& getArguments, const void* bodyData, - size_t bodySize); + size_t bodySize) ORTHANC_OVERRIDE; virtual bool InvokeService(SharedLibrary& plugin, _OrthancPluginService service, - const void* parameters); + const void* parameters) ORTHANC_OVERRIDE; - virtual void SignalChange(const ServerIndexChange& change); - + virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE; + virtual void SignalStoredInstance(const std::string& instanceId, DicomInstanceToStore& instance, - const Json::Value& simplifiedTags); + const Json::Value& simplifiedTags) ORTHANC_OVERRIDE; virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance, - const Json::Value& simplified) + const Json::Value& simplified) ORTHANC_OVERRIDE { return true; // TODO Enable filtering of instances from plugins } @@ -298,7 +302,7 @@ bool HasWorklistHandler(); - virtual IWorklistRequestHandler* ConstructWorklistRequestHandler(); + virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() ORTHANC_OVERRIDE; bool HasCustomImageDecoder(); @@ -311,22 +315,22 @@ virtual ImageAccessor* Decode(const void* dicom, size_t size, - unsigned int frame); + unsigned int frame) ORTHANC_OVERRIDE; virtual bool IsAllowed(HttpMethod method, const char* uri, const char* ip, const char* username, const IHttpHandler::Arguments& httpHeaders, - const IHttpHandler::GetArguments& getArguments); + const IHttpHandler::GetArguments& getArguments) ORTHANC_OVERRIDE; bool HasFindHandler(); - virtual IFindRequestHandler* ConstructFindRequestHandler(); + virtual IFindRequestHandler* ConstructFindRequestHandler() ORTHANC_OVERRIDE; bool HasMoveHandler(); - virtual IMoveRequestHandler* ConstructMoveRequestHandler(); + virtual IMoveRequestHandler* ConstructMoveRequestHandler() ORTHANC_OVERRIDE; IJob* UnserializeJob(const std::string& type, const Json::Value& value); @@ -340,7 +344,16 @@ const char* username, HttpMethod method, const UriComponents& uri, - const Arguments& headers); + const Arguments& headers) ORTHANC_OVERRIDE; + + // 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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Engine/PluginsEnumerations.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Engine/PluginsEnumerations.h Wed Apr 01 10:15:33 2020 +0200 @@ -79,6 +79,8 @@ OrthancPluginJobStepStatus Convert(JobStepCode step); JobStepCode Convert(OrthancPluginJobStepStatus step); + + StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason); } }
--- a/Plugins/Engine/PluginsJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Engine/PluginsJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Engine/PluginsJob.h Wed Apr 01 10:15:33 2020 +0200 @@ -51,30 +51,30 @@ virtual ~PluginsJob(); - virtual void Start() + virtual void Start() ORTHANC_OVERRIDE { } - virtual JobStepResult Step(); + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; - virtual void Reset(); + virtual void Reset() ORTHANC_OVERRIDE; - virtual void Stop(JobStopReason reason); + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - virtual float GetProgress(); + virtual float GetProgress() ORTHANC_OVERRIDE; - virtual void GetJobType(std::string& target) + virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE { target = type_; } - virtual void GetPublicContent(Json::Value& value); + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& value); + virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE; virtual bool GetOutput(std::string& output, MimeType& mime, - const std::string& key) + const std::string& key) ORTHANC_OVERRIDE { // TODO return false;
--- a/Plugins/Include/orthanc/OrthancCPlugin.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Wed Apr 01 10:15:33 2020 +0200 @@ -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. @@ -58,7 +59,7 @@ * @brief Functions to register and manage callbacks by the plugins. * * @defgroup DicomCallbacks DicomCallbacks - * @brief Functions to register and manage DICOM callbacks (worklists, C-Find, C-MOVE). + * @brief Functions to register and manage DICOM callbacks (worklists, C-FIND, C-MOVE, storage commitment). * * @defgroup Orthanc Orthanc * @brief Functions to access the content of the Orthanc server. @@ -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, + /*!< Success: The DICOM instance is properly stored in the SCP */ + + OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1, + /*!< 0110H: A general failure in processing the operation was encountered */ + + OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2, + /*!< 0112H: One or more of the elements in the Referenced SOP + Instance Sequence was not available */ + + OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3, + /*!< 0213H: The SCP does not currently have enough resources to + store the requested SOP Instance(s) */ + + OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4, + /*!< 0122H: Storage Commitment has been requested for a SOP + Instance with a SOP Class that is not supported by the SCP */ + + OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict = 5, + /*!< 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_DuplicateTransactionUID = 6 + /*!< 0131H: The Transaction UID of the Storage Commitment Request + is already in use */ + } OrthancPluginStorageCommitmentFailureReason; + /** @@ -1023,7 +1062,7 @@ * @brief Opaque structure to an object that can be used to check whether a DICOM instance matches a C-Find query. * @ingroup Toolbox **/ - typedef struct _OrthancPluginFindAnswers_t OrthancPluginFindMatcher; + typedef struct _OrthancPluginFindMatcher_t OrthancPluginFindMatcher; @@ -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; @@ -2738,7 +2778,7 @@ * @return The pointer to the DICOM data, NULL in case of error. * @ingroup Callbacks **/ - ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceData( + ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData( OrthancPluginContext* context, OrthancPluginDicomInstance* instance) { @@ -7259,6 +7299,117 @@ } + + /** + * @brief Callback executed by the storage commitment SCP. + * + * Signature of a factory function that creates an object to handle + * one incoming storage commitment request. + * + * @remark The factory receives the list of the SOP class/instance + * UIDs of interest to the remote storage commitment SCU. This gives + * the factory the possibility to start some prefetch process + * upfront in the background, before the handler object is actually + * queried about the status of these DICOM instances. + * + * @param handler Output variable where the factory puts the handler object it created. + * @param jobId ID of the Orthanc job that is responsible for handling + * the storage commitment request. This job will successively look for the + * status of all the individual queried DICOM instances. + * @param transactionUid UID of the storage commitment transaction + * provided by the storage commitment SCU. It contains the value of the + * (0008,1195) DICOM tag. + * @param sopClassUids Array of the SOP class UIDs (0008,0016) that are queried by the SCU. + * @param sopInstanceUids Array of the SOP instance UIDs (0008,0018) that are queried by the SCU. + * @param countInstances Number of DICOM instances that are queried. This is the size + * of the `sopClassUids` and `sopInstanceUids` arrays. + * @param remoteAet The AET of the storage commitment SCU. + * @param calledAet The AET used by the SCU to contact the storage commitment SCP (i.e. Orthanc). + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + 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); + + + /** + * @brief Callback to free one storage commitment SCP handler. + * + * Signature of a callback function that releases the resources + * allocated by the factory of the storage commitment SCP. The + * handler is the return value of a previous call to the + * OrthancPluginStorageCommitmentFactory() callback. + * + * @param handler The handler object to be destructed. + * @ingroup DicomCallbacks + **/ + typedef void (*OrthancPluginStorageCommitmentDestructor) (void* handler); + + + /** + * @brief Callback to get the status of one DICOM instance in the + * storage commitment SCP. + * + * Signature of a callback function that is successively invoked for + * each DICOM instance that is queried by the remote storage + * commitment SCU. The function must be tought of as a method of + * the handler object that was created by a previous call to the + * OrthancPluginStorageCommitmentFactory() callback. After each call + * to this method, the progress of the associated Orthanc job is + * updated. + * + * @param target Output variable where to put the status for the queried instance. + * @param handler The handler object associated with this storage commitment request. + * @param sopClassUid The SOP class UID (0008,0016) of interest. + * @param sopInstanceUid The SOP instance UID (0008,0018) of interest. + * @ingroup DicomCallbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentLookup) ( + OrthancPluginStorageCommitmentFailureReason* target, + void* handler, + const char* sopClassUid, + const char* sopInstanceUid); + + + typedef struct + { + OrthancPluginStorageCommitmentFactory factory; + OrthancPluginStorageCommitmentDestructor destructor; + OrthancPluginStorageCommitmentLookup lookup; + } _OrthancPluginRegisterStorageCommitmentScpCallback; + + /** + * @brief Register a callback to handle incoming requests to the storage commitment SCP. + * + * This function registers a callback to handle storage commitment SCP requests. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param factory Factory function that creates the handler object + * for incoming storage commitment requests. + * @param destructor Destructor function to destroy the handler object. + * @param lookup Callback method to get the status of one DICOM instance. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterStorageCommitmentScpCallback( + OrthancPluginContext* context, + OrthancPluginStorageCommitmentFactory factory, + OrthancPluginStorageCommitmentDestructor destructor, + OrthancPluginStorageCommitmentLookup lookup) + { + _OrthancPluginRegisterStorageCommitmentScpCallback params; + params.factory = factory; + params.destructor = destructor; + params.lookup = lookup; + return context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, ¶ms); + } #ifdef __cplusplus }
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -33,8 +33,9 @@ #include "OrthancPluginCppWrapper.h" +#include <boost/algorithm/string/predicate.hpp> +#include <boost/move/unique_ptr.hpp> #include <boost/thread.hpp> -#include <boost/algorithm/string/predicate.hpp> #include <json/reader.h> #include <json/writer.h> @@ -2168,7 +2169,7 @@ static const char* KEY_ASYNCHRONOUS = "Asynchronous"; static const char* KEY_PRIORITY = "Priority"; - std::auto_ptr<OrthancJob> protection(job); + boost::movelib::unique_ptr<OrthancJob> protection(job); if (body.type() != Json::objectValue) { @@ -3059,7 +3060,7 @@ } else { - std::auto_ptr<IChunkedRequestReader> reader(PostHandler(url, request)); + boost::movelib::unique_ptr<IChunkedRequestReader> reader(PostHandler(url, request)); if (reader.get() == NULL) { ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); @@ -3092,7 +3093,7 @@ } else { - std::auto_ptr<IChunkedRequestReader> reader(PutHandler(url, request)); + boost::movelib::unique_ptr<IChunkedRequestReader> reader(PutHandler(url, request)); if (reader.get() == NULL) { ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); @@ -3139,4 +3140,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 Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Apr 01 10:15:33 2020 +0200 @@ -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 @@ -297,7 +303,7 @@ public: OrthancConfiguration(); - OrthancConfiguration(bool load); + explicit OrthancConfiguration(bool load); const Json::Value& GetJson() const { @@ -363,7 +369,7 @@ public: OrthancImage(); - OrthancImage(OrthancPluginImage* image); + explicit OrthancImage(OrthancPluginImage* image); OrthancImage(OrthancPluginPixelFormat format, uint32_t width, @@ -429,15 +435,15 @@ uint32_t size); public: - FindMatcher(const OrthancPluginWorklistQuery* worklist); + explicit FindMatcher(const OrthancPluginWorklistQuery* worklist); - FindMatcher(const void* query, - uint32_t size) + FindMatcher(const void* query, + uint32_t size) { SetupDicom(query, size); } - FindMatcher(const MemoryBuffer& dicom) + explicit FindMatcher(const MemoryBuffer& dicom) { SetupDicom(dicom.GetData(), dicom.GetSize()); } @@ -804,7 +810,7 @@ boost::posix_time::ptime start_; public: - MetricsTimer(const char* name); + explicit MetricsTimer(const char* name); ~MetricsTimer(); }; @@ -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 }
--- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -21,6 +21,7 @@ #include "GdcmDecoderCache.h" +#include "../../../Core/Compatibility.h" #include "OrthancImageWrapper.h" namespace OrthancPlugins @@ -83,13 +84,13 @@ } // This is not the same image - std::auto_ptr<GdcmImageDecoder> decoder(new GdcmImageDecoder(dicom, size)); - std::auto_ptr<OrthancImageWrapper> image(new OrthancImageWrapper(context, decoder->Decode(context, frameIndex))); + std::unique_ptr<GdcmImageDecoder> decoder(new GdcmImageDecoder(dicom, size)); + std::unique_ptr<OrthancImageWrapper> image(new OrthancImageWrapper(context, decoder->Decode(context, frameIndex))); { // Cache the newly created decoder for further use boost::mutex::scoped_lock lock(mutex_); - decoder_ = decoder; + decoder_.reset(decoder.release()); size_ = size; md5_ = md5; }
--- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h Wed Apr 01 10:15:33 2020 +0200 @@ -21,6 +21,7 @@ #pragma once +#include "../../../Core/Compatibility.h" #include "GdcmImageDecoder.h" #include "OrthancImageWrapper.h" @@ -33,7 +34,7 @@ { private: boost::mutex mutex_; - std::auto_ptr<OrthancPlugins::GdcmImageDecoder> decoder_; + std::unique_ptr<OrthancPlugins::GdcmImageDecoder> decoder_; size_t size_; std::string md5_;
--- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -21,6 +21,7 @@ #include "GdcmImageDecoder.h" +#include "../../../Core/Compatibility.h" #include "OrthancImageWrapper.h" #include <gdcmImageReader.h> @@ -40,9 +41,9 @@ size_t size_; gdcm::ImageReader reader_; - std::auto_ptr<gdcm::ImageApplyLookupTable> lut_; - std::auto_ptr<gdcm::ImageChangePhotometricInterpretation> photometric_; - std::auto_ptr<gdcm::ImageChangePlanarConfiguration> interleaved_; + std::unique_ptr<gdcm::ImageApplyLookupTable> lut_; + std::unique_ptr<gdcm::ImageChangePhotometricInterpretation> photometric_; + std::unique_ptr<gdcm::ImageChangePlanarConfiguration> interleaved_; std::string decoded_; PImpl(const void* dicom,
--- a/Plugins/Samples/GdcmDecoder/Plugin.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/Plugin.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -19,6 +19,7 @@ **/ +#include "../../../Core/Compatibility.h" #include "GdcmDecoderCache.h" #include "OrthancImageWrapper.h" @@ -35,7 +36,7 @@ { try { - std::auto_ptr<OrthancPlugins::OrthancImageWrapper> image; + std::unique_ptr<OrthancPlugins::OrthancImageWrapper> image; #if 0 // Do not use the cache
--- a/Plugins/Samples/ModalityWorklists/Plugin.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -19,6 +19,7 @@ **/ +#include "../../../Core/Compatibility.h" #include "../Common/OrthancPluginCppWrapper.h" #include <boost/filesystem.hpp> @@ -142,7 +143,7 @@ try { // Construct an object to match the worklists in the database against the C-Find query - std::auto_ptr<OrthancPlugins::FindMatcher> matcher(CreateMatcher(query, issuerAet)); + std::unique_ptr<OrthancPlugins::FindMatcher> matcher(CreateMatcher(query, issuerAet)); // Loop over the regular files in the database folder namespace fs = boost::filesystem;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/README.txt Wed Apr 01 10:15:33 2020 +0200 @@ -0,0 +1,3 @@ +More contributed samples of plugins can be found and added in +the "OrthancContributed" repository on GitHub: +https://github.com/jodogne/OrthancContributed/tree/master/Plugins
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt Wed Apr 01 10:15:33 2020 +0200 @@ -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 Wed Apr 01 10:15:33 2020 +0200 @@ -0,0 +1,116 @@ +/** + * 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) +{ + /*std::string s; + OrthancPlugins::RestApiPost(s, "/jobs/" + std::string(jobId) + "/pause", NULL, 0, false);*/ + + 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/CMake/BoostConfiguration.cmake Wed Apr 01 10:14:49 2020 +0200 +++ b/Resources/CMake/BoostConfiguration.cmake Wed Apr 01 10:15:33 2020 +0200 @@ -12,7 +12,7 @@ endif() list(APPEND ORTHANC_BOOST_COMPONENTS filesystem thread system date_time regex) - find_package(Boost COMPONENTS "${ORTHANC_BOOST_COMPONENTS}") + find_package(Boost COMPONENTS ${ORTHANC_BOOST_COMPONENTS}) if (NOT Boost_FOUND) foreach (item ${ORTHANC_BOOST_COMPONENTS})
--- a/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake Wed Apr 01 10:14:49 2020 +0200 +++ b/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake Wed Apr 01 10:15:33 2020 +0200 @@ -185,11 +185,26 @@ ) -if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows" AND - CMAKE_COMPILER_IS_GNUCXX) - # This is MinGW +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + # For compatibility with Windows XP, avoid using fiber-local-storage + # in log4cplus, but use thread-local-storage instead. Otherwise, + # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll add_definitions( -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS - -DDCMTK_LOG4CPLUS_SINGLE_THREADED ) + + if (CMAKE_COMPILER_IS_GNUCXX OR # MinGW + "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # MSVC for 32bit (*) + + # (*) With multithreaded logging enabled, Visual Studio 2008 fails + # with error: ".\dcmtk-3.6.5\oflog\libsrc\globinit.cc(422) : error + # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot + # convert parameter 1 from 'void (__stdcall *)(void *)' to + # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'" + # None of the functions with this name in scope match the target type + + add_definitions( + -DDCMTK_LOG4CPLUS_SINGLE_THREADED + ) + endif() endif()
--- a/Resources/Configuration.json Wed Apr 01 10:14:49 2020 +0200 +++ b/Resources/Configuration.json Wed Apr 01 10:15:33 2020 +0200 @@ -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", @@ -221,7 +221,8 @@ // "AllowEcho" : false, // "AllowFind" : false, // "AllowMove" : false, - // "AllowStore" : true + // "AllowStore" : true, + // "AllowStorageCommitment" : false // new in 1.6.0 //} }, @@ -527,5 +528,9 @@ // Set the default private creator that is used by Orthanc when it // looks for a private tag in its dictionary (cf. "Dictionary" // option), or when it creates/modifies a DICOM file (new in Orthanc 1.6.0). - "DefaultPrivateCreator" : "" + "DefaultPrivateCreator" : "", + + // Maximum number of storage commitment reports (i.e. received from + // remote modalities) to be kept in memory (new in Orthanc 1.6.0). + "StorageCommitmentReportsSize" : 100 }
--- a/Resources/DicomTransferSyntaxes.json Wed Apr 01 10:14:49 2020 +0200 +++ b/Resources/DicomTransferSyntaxes.json Wed Apr 01 10:15:33 2020 +0200 @@ -40,6 +40,7 @@ "Retired" : false, "Note" : "Default Transfer Syntax for Lossy JPEG 8-bit Image Compression", "DCMTK" : "EXS_JPEGProcess1", + "DCMTK360" : "EXS_JPEGProcess1TransferSyntax", "GDCM" : "gdcm::TransferSyntax::JPEGBaselineProcess1" }, @@ -50,6 +51,7 @@ "Retired" : false, "Note" : "Default Transfer Syntax for Lossy JPEG (lossy, 8/12 bit), 12-bit Image Compression (Process 4 only)", "DCMTK" : "EXS_JPEGProcess2_4", + "DCMTK360" : "EXS_JPEGProcess2_4TransferSyntax", "GDCM" : "gdcm::TransferSyntax::JPEGExtendedProcess2_4" }, @@ -58,7 +60,8 @@ "Name" : "JPEG Extended Sequential (lossy, 8/12 bit), arithmetic coding", "Value" : "JPEGProcess3_5", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess3_5" + "DCMTK" : "EXS_JPEGProcess3_5", + "DCMTK360" : "EXS_JPEGProcess3_5TransferSyntax" }, { @@ -66,7 +69,8 @@ "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit)", "Value" : "JPEGProcess6_8", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess6_8" + "DCMTK" : "EXS_JPEGProcess6_8", + "DCMTK360" : "EXS_JPEGProcess6_8TransferSyntax" }, { @@ -74,7 +78,8 @@ "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit), arithmetic coding", "Value" : "JPEGProcess7_9", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess7_9" + "DCMTK" : "EXS_JPEGProcess7_9", + "DCMTK360" : "EXS_JPEGProcess7_9TransferSyntax" }, { @@ -82,7 +87,8 @@ "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit)", "Value" : "JPEGProcess10_12", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess10_12" + "DCMTK" : "EXS_JPEGProcess10_12", + "DCMTK360" : "EXS_JPEGProcess10_12TransferSyntax" }, { @@ -90,7 +96,8 @@ "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit), arithmetic coding", "Value" : "JPEGProcess11_13", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess11_13" + "DCMTK" : "EXS_JPEGProcess11_13", + "DCMTK360" : "EXS_JPEGProcess11_13TransferSyntax" }, { @@ -99,6 +106,7 @@ "Value" : "JPEGProcess14", "Retired" : false, "DCMTK" : "EXS_JPEGProcess14", + "DCMTK360" : "EXS_JPEGProcess14TransferSyntax", "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14" }, @@ -107,7 +115,8 @@ "Name" : "JPEG Lossless with any selection value, arithmetic coding", "Value" : "JPEGProcess15", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess15" + "DCMTK" : "EXS_JPEGProcess15", + "DCMTK360" : "EXS_JPEGProcess15TransferSyntax" }, { @@ -115,7 +124,8 @@ "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit)", "Value" : "JPEGProcess16_18", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess16_18" + "DCMTK" : "EXS_JPEGProcess16_18", + "DCMTK360" : "EXS_JPEGProcess16_18TransferSyntax" }, { @@ -123,7 +133,8 @@ "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit), arithmetic coding", "Value" : "JPEGProcess17_19", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess17_19" + "DCMTK" : "EXS_JPEGProcess17_19", + "DCMTK360" : "EXS_JPEGProcess17_19TransferSyntax" }, { @@ -131,7 +142,8 @@ "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit)", "Value" : "JPEGProcess20_22", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess20_22" + "DCMTK" : "EXS_JPEGProcess20_22", + "DCMTK360" : "EXS_JPEGProcess20_22TransferSyntax" }, { @@ -139,7 +151,8 @@ "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit), arithmetic coding", "Value" : "JPEGProcess21_23", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess21_23" + "DCMTK" : "EXS_JPEGProcess21_23", + "DCMTK360" : "EXS_JPEGProcess21_23TransferSyntax" }, { @@ -147,7 +160,8 @@ "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit)", "Value" : "JPEGProcess24_26", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess24_26" + "DCMTK" : "EXS_JPEGProcess24_26", + "DCMTK360" : "EXS_JPEGProcess24_26TransferSyntax" }, { @@ -155,7 +169,8 @@ "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit), arithmetic coding", "Value" : "JPEGProcess25_27", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess25_27" + "DCMTK" : "EXS_JPEGProcess25_27", + "DCMTK360" : "EXS_JPEGProcess25_27TransferSyntax" }, { @@ -163,7 +178,8 @@ "Name" : "JPEG Lossless, Hierarchical", "Value" : "JPEGProcess28", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess28" + "DCMTK" : "EXS_JPEGProcess28", + "DCMTK360" : "EXS_JPEGProcess28TransferSyntax" }, { @@ -171,7 +187,8 @@ "Name" : "JPEG Lossless, Hierarchical, arithmetic coding", "Value" : "JPEGProcess29", "Retired" : true, - "DCMTK" : "EXS_JPEGProcess29" + "DCMTK" : "EXS_JPEGProcess29", + "DCMTK360" : "EXS_JPEGProcess29TransferSyntax" }, { @@ -181,6 +198,7 @@ "Retired" : false, "Note" : "Default Transfer Syntax for Lossless JPEG Image Compression", "DCMTK" : "EXS_JPEGProcess14SV1", + "DCMTK360" : "EXS_JPEGProcess14SV1TransferSyntax", "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14_1" }, @@ -275,7 +293,8 @@ "Name" : "MPEG4 High Profile / Level 4.1", "Value" : "MPEG4HighProfileLevel4_1", "Retired" : false, - "DCMTK" : "EXS_MPEG4HighProfileLevel4_1" + "DCMTK" : "EXS_MPEG4HighProfileLevel4_1", + "SinceDCMTK" : "361" }, { @@ -283,7 +302,8 @@ "Name" : "MPEG4 BD-compatible High Profile / Level 4.1", "Value" : "MPEG4BDcompatibleHighProfileLevel4_1", "Retired" : false, - "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1" + "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1", + "SinceDCMTK" : "361" }, { @@ -291,7 +311,8 @@ "Name" : "MPEG4 High Profile / Level 4.2 For 2D Video", "Value" : "MPEG4HighProfileLevel4_2_For2DVideo", "Retired" : false, - "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo" + "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo", + "SinceDCMTK" : "361" }, { @@ -299,7 +320,8 @@ "Name" : "MPEG4 High Profile / Level 4.2 For 3D Video", "Value" : "MPEG4HighProfileLevel4_2_For3DVideo", "Retired" : false, - "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo" + "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo", + "SinceDCMTK" : "361" }, { @@ -307,7 +329,8 @@ "Name" : "1.2.840.10008.1.2.4.106", "Value" : "MPEG4StereoHighProfileLevel4_2", "Retired" : false, - "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2" + "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2", + "SinceDCMTK" : "361" }, {
--- a/Resources/DownloadOrthancFramework.cmake Wed Apr 01 10:14:49 2020 +0200 +++ b/Resources/DownloadOrthancFramework.cmake Wed Apr 01 10:15:33 2020 +0200 @@ -112,6 +112,8 @@ set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.8") set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0") + set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25") # Below this point are development snapshots that were used to # release some plugin, before an official release of the Orthanc
--- a/Resources/ErrorCodes.json Wed Apr 01 10:14:49 2020 +0200 +++ b/Resources/ErrorCodes.json Wed Apr 01 10:15:33 2020 +0200 @@ -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/Resources/GenerateTransferSyntaxesDcmtk.mustache Wed Apr 01 10:14:49 2020 +0200 +++ b/Resources/GenerateTransferSyntaxesDcmtk.mustache Wed Apr 01 10:15:33 2020 +0200 @@ -34,10 +34,10 @@ namespace Orthanc { - bool GetDcmtkTransferSyntax(E_TransferSyntax& target, - DicomTransferSyntax syntax) + bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target, + DicomTransferSyntax source) { - switch (syntax) + switch (source) { {{#Syntaxes}} {{#DCMTK}} @@ -45,7 +45,50 @@ #if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}} {{/SinceDCMTK}} case DicomTransferSyntax_{{Value}}: + {{#DCMTK360}} +# if DCMTK_VERSION_NUMBER <= 360 + target = {{DCMTK360}}; +# else target = {{DCMTK}}; +# endif + {{/DCMTK360}} + {{^DCMTK360}} + target = {{DCMTK}}; + {{/DCMTK360}} + return true; + {{#SinceDCMTK}} +#endif + {{/SinceDCMTK}} + + {{/DCMTK}} + {{/Syntaxes}} + default: + return false; + } + } + + + bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target, + E_TransferSyntax source) + { + switch (source) + { + {{#Syntaxes}} + {{#DCMTK}} + {{#SinceDCMTK}} +#if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}} + {{/SinceDCMTK}} + {{#DCMTK360}} +# if DCMTK_VERSION_NUMBER <= 360 + case {{DCMTK360}}: +# else + case {{DCMTK}}: +# endif + {{/DCMTK360}} + {{^DCMTK360}} + case {{DCMTK}}: + {{/DCMTK360}} + target = DicomTransferSyntax_{{Value}}; return true; {{#SinceDCMTK}} #endif
--- a/Resources/Patches/dcmtk-3.6.5.patch Wed Apr 01 10:14:49 2020 +0200 +++ b/Resources/Patches/dcmtk-3.6.5.patch Wed Apr 01 10:15:33 2020 +0200 @@ -1,6 +1,6 @@ diff -urEb dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h ---- dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h 2020-03-05 19:43:37.678302817 +0100 -+++ dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h 2020-03-05 19:43:41.198312828 +0100 +--- dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h 2020-03-18 10:22:41.555166774 +0100 ++++ dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h 2020-03-18 10:22:53.395131056 +0100 @@ -152,6 +152,12 @@ /// returns an iterator to the end of the repeating tag dictionary DcmDictEntryListIterator repeatingEnd() { return repDict.end(); } @@ -15,8 +15,8 @@ /** private undefined assignment operator diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc ---- dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc 2020-03-05 19:43:37.682302828 +0100 -+++ dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc 2020-03-05 19:43:41.198312828 +0100 +--- dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc 2020-03-18 10:22:41.559166762 +0100 ++++ dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc 2020-03-18 10:22:53.395131056 +0100 @@ -900,3 +900,6 @@ wrlock().clear(); wrunlock(); @@ -25,8 +25,8 @@ + +#include "dcdict_orthanc.cc" diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc ---- dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc 2020-03-05 19:43:37.682302828 +0100 -+++ dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc 2020-03-05 19:43:41.198312828 +0100 +--- dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc 2020-03-18 10:22:41.559166762 +0100 ++++ dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc 2020-03-18 10:22:53.395131056 +0100 @@ -36,6 +36,9 @@ #include "dcmtk/dcmdata/dcostrma.h" /* for class DcmOutputStream */ #include "dcmtk/dcmdata/dcwcache.h" /* for class DcmWriteCache */ @@ -38,8 +38,8 @@ // ******************************** diff -urEb dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h ---- dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h 2020-03-05 19:43:37.686302839 +0100 -+++ dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h 2020-03-05 19:43:41.198312828 +0100 +--- dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h 2020-03-18 10:22:41.543166810 +0100 ++++ dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h 2020-03-18 10:22:53.395131056 +0100 @@ -63,7 +63,7 @@ DCMTK_LOG4CPLUS_INLINE_EXPORT @@ -86,22 +86,22 @@ diff -urEb dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc dcmtk-3.6.5/oflog/libsrc/oflog.cc ---- dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc 2020-03-05 19:43:37.690302851 +0100 -+++ dcmtk-3.6.5/oflog/libsrc/oflog.cc 2020-03-05 19:43:54.622350144 +0100 +--- dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc 2020-03-18 10:22:41.547166798 +0100 ++++ dcmtk-3.6.5/oflog/libsrc/oflog.cc 2020-03-18 11:55:50.116856932 +0100 @@ -19,6 +19,10 @@ * */ -+#ifdef __MINGW32__ -+# include <winsock.h> ++#if defined(_WIN32) ++# include <winsock2.h> +#endif + #include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */ #include "dcmtk/oflog/oflog.h" diff -urEb dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h ---- dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h 2020-03-05 19:43:37.714302919 +0100 -+++ dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h 2020-03-05 19:43:41.198312828 +0100 +--- dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h 2020-03-18 10:22:41.587166677 +0100 ++++ dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h 2020-03-18 10:22:53.395131056 +0100 @@ -575,7 +575,7 @@ */ void setlinebuf()
--- a/Resources/Patches/dcmtk.txt Wed Apr 01 10:14:49 2020 +0200 +++ b/Resources/Patches/dcmtk.txt Wed Apr 01 10:15:33 2020 +0200 @@ -4,6 +4,7 @@ diff -urEb dcmtk-3.6.0.orig/ dcmtk-3.6.0 diff -urEb dcmtk-3.6.2.orig/ dcmtk-3.6.2 diff -urEb dcmtk-3.6.4.orig/ dcmtk-3.6.4 +diff -urEb dcmtk-3.6.5.orig/ dcmtk-3.6.5 For "dcmtk-3.6.2-private.dic" =============================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/README.txt Wed Apr 01 10:15:33 2020 +0200 @@ -0,0 +1,7 @@ +More contributed samples and documentation can be found and added in +the "OrthancContributed" repository on GitHub: +https://github.com/jodogne/OrthancContributed + +The integration tests of Orthanc provide a lot of samples about the +features of the REST API of Orthanc: +https://bitbucket.org/sjodogne/orthanc-tests/src/default/Tests/Tests.py
--- a/UnitTestsSources/FromDcmtkTests.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/UnitTestsSources/FromDcmtkTests.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -1917,7 +1917,186 @@ #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 +#include "../Core/DicomParsing/Internals/DicomFrameIndex.h" + #include <dcmtk/dcmdata/dcostrmb.h> +#include <dcmtk/dcmdata/dcpixel.h> +#include <dcmtk/dcmdata/dcpxitem.h> + + +namespace Orthanc +{ + class IDicomTranscoder : public boost::noncopyable + { + public: + virtual ~IDicomTranscoder() + { + } + + virtual DicomTransferSyntax GetTransferSyntax() = 0; + + virtual std::string GetSopClassUid() = 0; + + virtual std::string GetSopInstanceUid() = 0; + + virtual unsigned int GetFramesCount() = 0; + + virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0; + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) = 0; + + virtual IDicomTranscoder* Transcode(std::set<DicomTransferSyntax> syntaxes, + bool allowNewSopInstanceUid) = 0; + }; + + + class DcmtkTranscoder : public IDicomTranscoder + { + private: + std::unique_ptr<DcmFileFormat> dicom_; + std::unique_ptr<DicomFrameIndex> index_; + DicomTransferSyntax transferSyntax_; + std::string sopClassUid_; + std::string sopInstanceUid_; + + void Setup(DcmFileFormat* dicom) + { + dicom_.reset(dicom); + + if (dicom == NULL || + dicom_->getDataset() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + DcmDataset& dataset = *dicom_->getDataset(); + index_.reset(new DicomFrameIndex(dataset)); + + E_TransferSyntax xfer = dataset.getOriginalXfer(); + if (xfer == EXS_Unknown) + { + dataset.updateOriginalXfer(); + xfer = dataset.getOriginalXfer(); + if (xfer == EXS_Unknown) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot determine the transfer syntax of the DICOM instance"); + } + } + + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer)) + { + throw OrthancException( + ErrorCode_BadFileFormat, + "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer)); + } + + const char* a = NULL; + const char* b = NULL; + + if (!dataset.findAndGetString(DCM_SOPClassUID, a).good() || + !dataset.findAndGetString(DCM_SOPInstanceUID, b).good() || + a == NULL || + b == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing SOP class/instance UID in DICOM instance"); + } + + sopClassUid_.assign(a); + sopInstanceUid_.assign(b); + } + + public: + DcmtkTranscoder(DcmFileFormat* dicom) // Takes ownership + { + Setup(dicom); + } + + DcmtkTranscoder(const void* dicom, + size_t size) + { + Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size)); + } + + virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE + { + return transferSyntax_; + } + + virtual std::string GetSopClassUid() ORTHANC_OVERRIDE + { + return sopClassUid_; + } + + virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE + { + return sopInstanceUid_; + } + + virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE + { + return index_->GetFramesCount(); + } + + virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE + { + assert(dicom_->getDataset() != NULL); + return DicomImageDecoder::Decode(*dicom_->getDataset(), frame); + } + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) ORTHANC_OVERRIDE + { +#if 1 + index_->GetRawFrame(target, frame); + printf("%d: %d\n", frame, target.size()); +#endif + +#if 1 + assert(dicom_->getDataset() != NULL); + DcmDataset& dataset = *dicom_->getDataset(); + + DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset); + + if (pixelSequence != NULL && + frame == 0 && + pixelSequence->card() != GetFramesCount() + 1) + { + printf("COMPRESSED\n"); + + // Check out "djcodecd.cc" + + printf("%d fragments\n", pixelSequence->card()); + + // Skip the first fragment, that is the offset table + for (unsigned long i = 1; ;i++) + { + DcmPixelItem *fragment = NULL; + if (pixelSequence->getItem(fragment, i).good()) + { + printf("fragment %d %d\n", i, fragment->getLength()); + } + else + { + break; + } + } + } +#endif + } + + virtual IDicomTranscoder* Transcode(std::set<DicomTransferSyntax> syntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + }; +} + + + static bool Transcode(std::string& buffer, DcmDataset& dataSet, @@ -1991,44 +2170,97 @@ #include "dcmtk/dcmjpeg/djrploss.h" /* for DJ_RPLossy */ #include "dcmtk/dcmjpeg/djrplol.h" /* for DJ_RPLossless */ +#include <boost/filesystem.hpp> + + +static void TestFile(const std::string& path) +{ + printf("** %s\n", path.c_str()); + + std::string s; + SystemToolbox::ReadFile(s, path); + + Orthanc::DcmtkTranscoder transcoder(s.c_str(), s.size()); + + printf("[%s] [%s] [%s] %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()), + transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(), + transcoder.GetFramesCount()); + + for (size_t i = 0; i < transcoder.GetFramesCount(); i++) + { + std::string f; + transcoder.GetCompressedFrame(f, i); + + if (i == 0) + { + static unsigned int i = 0; + char buf[1024]; + sprintf(buf, "/tmp/frame-%06d.dcm", i++); + printf(">> %s\n", buf); + Orthanc::SystemToolbox::WriteFile(f, buf); + } + } + + printf("\n"); +} + TEST(Toto, Transcode) { - OFLog::configure(OFLogger::DEBUG_LOG_LEVEL); - std::string s; - //SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm"); - //SystemToolbox::ReadFile(s, "/home/jodogne/DICOM/Alain.dcm"); - SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/Brainix/Epi/IM-0001-0002.dcm"); + if (0) + { + OFLog::configure(OFLogger::DEBUG_LOG_LEVEL); - std::auto_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(s.c_str(), s.size())); + std::string s; + //SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm"); + //SystemToolbox::ReadFile(s, "/home/jodogne/DICOM/Alain.dcm"); + SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/Brainix/Epi/IM-0001-0002.dcm"); - // less /home/jodogne/Downloads/dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcxfer.h - printf(">> %d\n", dicom->getDataset()->getOriginalXfer()); // => 4 == EXS_JPEGProcess1 + std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(s.c_str(), s.size())); - const DcmRepresentationParameter *p; + // less /home/jodogne/Downloads/dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcxfer.h + printf(">> %d\n", dicom->getDataset()->getOriginalXfer()); // => 4 == EXS_JPEGProcess1 + + const DcmRepresentationParameter *p; #if 0 - E_TransferSyntax target = EXS_LittleEndianExplicit; - p = NULL; + E_TransferSyntax target = EXS_LittleEndianExplicit; + p = NULL; #elif 1 - E_TransferSyntax target = EXS_JPEGProcess14SV1; - DJ_RPLossless rp_lossless(6, 0); - p = &rp_lossless; + E_TransferSyntax target = EXS_JPEGProcess14SV1; + DJ_RPLossless rp_lossless(6, 0); + p = &rp_lossless; #else - E_TransferSyntax target = EXS_JPEGProcess1; - DJ_RPLossy rp_lossy(90); // quality - p = &rp_lossy; + E_TransferSyntax target = EXS_JPEGProcess1; + DJ_RPLossy rp_lossy(90); // quality + p = &rp_lossy; #endif - //E_TransferSyntax target = EXS_LittleEndianImplicit; - - ASSERT_TRUE(dicom->getDataset()->chooseRepresentation(target, p).good()); - ASSERT_TRUE(dicom->getDataset()->canWriteXfer(target)); + ASSERT_TRUE(dicom->getDataset()->chooseRepresentation(target, p).good()); + ASSERT_TRUE(dicom->getDataset()->canWriteXfer(target)); + + std::string t; + ASSERT_TRUE(Transcode(t, *dicom->getDataset(), target)); + + SystemToolbox::WriteFile(s, "source.dcm"); + SystemToolbox::WriteFile(t, "target.dcm"); + } - std::string t; - ASSERT_TRUE(Transcode(t, *dicom->getDataset(), target)); + if (1) + { + const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes"; + + for (boost::filesystem::directory_iterator it(PATH); + it != boost::filesystem::directory_iterator(); ++it) + { + if (boost::filesystem::is_regular_file(it->status())) + { + TestFile(it->path().string()); + } + } - SystemToolbox::WriteFile(s, "source.dcm"); - SystemToolbox::WriteFile(t, "target.dcm"); + TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm"); + TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm"); + } } #endif
--- a/UnitTestsSources/MemoryCacheTests.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/UnitTestsSources/MemoryCacheTests.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -44,6 +44,7 @@ #include "../Core/Cache/SharedArchive.h" #include "../Core/IDynamicObject.h" #include "../Core/Logging.h" +#include "../OrthancServer/StorageCommitmentReports.h" TEST(LRU, Basic) @@ -366,3 +367,94 @@ ASSERT_FALSE(c.Fetch(v, "hello")); ASSERT_TRUE(c.Fetch(v, "hello2")); ASSERT_EQ("b", v); } + + +TEST(StorageCommitmentReports, Basic) +{ + Orthanc::StorageCommitmentReports reports(2); + ASSERT_EQ(2u, reports.GetMaxSize()); + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "nope"); + ASSERT_EQ("nope", accessor.GetTransactionUid()); + ASSERT_FALSE(accessor.IsValid()); + ASSERT_THROW(accessor.GetReport(), Orthanc::OrthancException); + } + + reports.Store("a", new Orthanc::StorageCommitmentReports::Report("aet_a")); + reports.Store("b", new Orthanc::StorageCommitmentReports::Report("aet_b")); + reports.Store("c", new Orthanc::StorageCommitmentReports::Report("aet_c")); + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a"); + ASSERT_FALSE(accessor.IsValid()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b"); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("aet_b", accessor.GetReport().GetRemoteAet()); + ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Pending, + accessor.GetReport().GetStatus()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c"); + ASSERT_EQ("aet_c", accessor.GetReport().GetRemoteAet()); + ASSERT_TRUE(accessor.IsValid()); + } + + { + std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report + (new Orthanc::StorageCommitmentReports::Report("aet")); + report->AddSuccess("class1", "instance1"); + report->AddFailure("class2", "instance2", + Orthanc::StorageCommitmentFailureReason_ReferencedSOPClassNotSupported); + report->MarkAsComplete(); + reports.Store("a", report.release()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a"); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet()); + ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Failure, + accessor.GetReport().GetStatus()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b"); + ASSERT_FALSE(accessor.IsValid()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c"); + ASSERT_TRUE(accessor.IsValid()); + } + + { + std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report + (new Orthanc::StorageCommitmentReports::Report("aet")); + report->AddSuccess("class1", "instance1"); + report->MarkAsComplete(); + reports.Store("a", report.release()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a"); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet()); + ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Success, + accessor.GetReport().GetStatus()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b"); + ASSERT_FALSE(accessor.IsValid()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c"); + ASSERT_TRUE(accessor.IsValid()); + } +}
--- a/UnitTestsSources/MultiThreadingTests.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -102,7 +102,7 @@ { } - virtual JobStepResult Step() ORTHANC_OVERRIDE + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE { if (fails_) { @@ -1046,12 +1046,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; @@ -1102,7 +1102,7 @@ lock.SetTrailingOperationTimeout(300); } - ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode()); { GenericJobUnserializer unserializer; @@ -1619,8 +1619,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()); @@ -1678,8 +1678,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)); @@ -1747,7 +1747,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()); @@ -1756,7 +1756,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_THROW(job.Step(), OrthancException); + ASSERT_THROW(job.Step("jobId"), OrthancException); } { @@ -1778,7 +1778,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()); @@ -1787,7 +1787,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()); @@ -1796,7 +1796,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_THROW(job.Step(), OrthancException); + ASSERT_THROW(job.Step("jobId"), OrthancException); } { @@ -1819,7 +1819,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()); @@ -1828,7 +1828,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_THROW(job.Step(), OrthancException); + ASSERT_THROW(job.Step("jobId"), OrthancException); } { @@ -1853,7 +1853,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()); @@ -1862,7 +1862,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()); @@ -1871,7 +1871,7 @@ ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); } - ASSERT_THROW(job.Step(), OrthancException); + ASSERT_THROW(job.Step("jobId"), OrthancException); } } @@ -1898,6 +1898,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; @@ -1926,6 +1928,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"; @@ -1945,8 +1949,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) @@ -1975,4 +1981,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 Wed Apr 01 10:14:49 2020 +0200 +++ b/UnitTestsSources/ToolboxTests.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -138,6 +138,29 @@ #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)); +} + + TEST(Toolbox, UniquePtr) { std::unique_ptr<int> i(new int(42));