Mercurial > hg > orthanc
changeset 3825:4570c57668a8
refactoring DicomUserConnection as Dicom[Control|Store]UserConnection
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 10 Apr 2020 16:04:54 +0200 |
parents | 37e20bbf25f5 |
children | e82bd07c384e |
files | Core/DicomNetworking/DicomAssociation.cpp Core/DicomNetworking/DicomAssociation.h Core/DicomNetworking/DicomAssociationParameters.cpp Core/DicomNetworking/DicomAssociationParameters.h Core/DicomNetworking/DicomControlUserConnection.cpp Core/DicomNetworking/DicomControlUserConnection.h Core/DicomNetworking/DicomStoreUserConnection.cpp Core/DicomNetworking/DicomStoreUserConnection.h Core/Enumerations.h Resources/CMake/OrthancFrameworkConfiguration.cmake |
diffstat | 10 files changed, 2417 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomAssociation.cpp Fri Apr 10 16:04:54 2020 +0200 @@ -0,0 +1,856 @@ +/** + * 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 "../PrecompiledHeaders.h" +#include "DicomAssociation.h" + +#if !defined(DCMTK_VERSION_NUMBER) +# error The macro DCMTK_VERSION_NUMBER must be defined +#endif + +#include "../Logging.h" +#include "../OrthancException.h" + +#include <dcmtk/dcmnet/diutil.h> // For dcmConnectionTimeout() +#include <dcmtk/dcmdata/dcdeftag.h> + +namespace Orthanc +{ + 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 DicomAssociation::Initialize() + { + role_ = DicomAssociationRole_Default; + isOpen_ = false; + net_ = NULL; + params_ = NULL; + assoc_ = NULL; + + // Must be after "isOpen_ = false" + ClearPresentationContexts(); + } + + + void DicomAssociation::CheckConnecting(const DicomAssociationParameters& parameters, + const OFCondition& cond) + { + try + { + CheckCondition(cond, parameters, "connecting"); + } + catch (OrthancException&) + { + CloseInternal(); + throw; + } + } + + + void DicomAssociation::CloseInternal() + { + if (assoc_ != NULL) + { + ASC_releaseAssociation(assoc_); + ASC_destroyAssociation(&assoc_); + assoc_ = NULL; + params_ = NULL; + } + else + { + if (params_ != NULL) + { + ASC_destroyAssociationParameters(¶ms_); + params_ = NULL; + } + } + + if (net_ != NULL) + { + ASC_dropNetwork(&net_); + net_ = NULL; + } + + accepted_.clear(); + isOpen_ = false; + } + + + void DicomAssociation::AddAccepted(const std::string& abstractSyntax, + DicomTransferSyntax syntax, + uint8_t presentationContextId) + { + AcceptedPresentationContexts::iterator found = accepted_.find(abstractSyntax); + + if (found == accepted_.end()) + { + std::map<DicomTransferSyntax, uint8_t> syntaxes; + syntaxes[syntax] = presentationContextId; + accepted_[abstractSyntax] = syntaxes; + } + else + { + if (found->second.find(syntax) != found->second.end()) + { + LOG(WARNING) << "The same transfer syntax (" + << GetTransferSyntaxUid(syntax) + << ") was accepted twice for the same abstract syntax UID (" + << abstractSyntax << ")"; + } + else + { + found->second[syntax] = presentationContextId; + } + } + } + + + DicomAssociation::~DicomAssociation() + { + try + { + Close(); + } + catch (OrthancException&) + { + // Don't throw exception in destructors + } + } + + + void DicomAssociation::SetRole(DicomAssociationRole role) + { + if (role_ != role) + { + Close(); + role_ = role; + } + } + + + void DicomAssociation::ClearPresentationContexts() + { + Close(); + proposed_.clear(); + proposed_.reserve(MAX_PROPOSED_PRESENTATIONS); + } + + + void DicomAssociation::Open(const DicomAssociationParameters& parameters) + { + if (isOpen_) + { + return; // Already open + } + + // Timeout used during association negociation and ASC_releaseAssociation() + uint32_t acseTimeout = parameters.GetTimeout(); + if (acseTimeout == 0) + { + /** + * Timeout is disabled. Global timeout (seconds) for + * connecting to remote hosts. Default value is -1 which + * selects infinite timeout, i.e. blocking connect(). + **/ + dcmConnectionTimeout.set(-1); + acseTimeout = 10; + } + else + { + dcmConnectionTimeout.set(acseTimeout); + } + + T_ASC_SC_ROLE dcmtkRole; + switch (role_) + { + case DicomAssociationRole_Default: + dcmtkRole = ASC_SC_ROLE_DEFAULT; + break; + + case DicomAssociationRole_Scu: + dcmtkRole = ASC_SC_ROLE_SCU; + break; + + case DicomAssociationRole_Scp: + dcmtkRole = ASC_SC_ROLE_SCP; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + assert(net_ == NULL && + params_ == NULL && + assoc_ == NULL); + + if (proposed_.empty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "No presentation context was proposed"); + } + + LOG(INFO) << "Opening a DICOM SCU connection from AET \"" + << parameters.GetLocalApplicationEntityTitle() + << "\" to AET \"" << parameters.GetRemoteApplicationEntityTitle() + << "\" on host " << parameters.GetRemoteHost() + << ":" << parameters.GetRemotePort() + << " (manufacturer: " << EnumerationToString(parameters.GetRemoteManufacturer()) << ")"; + + CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_)); + CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); + + // Set this application's title and the called application's title in the params + CheckConnecting(parameters, ASC_setAPTitles( + params_, parameters.GetLocalApplicationEntityTitle().c_str(), + parameters.GetRemoteApplicationEntityTitle().c_str(), NULL)); + + // Set the network addresses of the local and remote entities + char localHost[HOST_NAME_MAX]; + gethostname(localHost, HOST_NAME_MAX - 1); + + char remoteHostAndPort[HOST_NAME_MAX]; + +#ifdef _MSC_VER + _snprintf +#else + snprintf +#endif + (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", + parameters.GetRemoteHost().c_str(), parameters.GetRemotePort()); + + CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort)); + + // Set various options + CheckConnecting(parameters, ASC_setTransportLayerType(params_, /*opt_secureConnection*/ false)); + + // Setup the list of proposed presentation contexts + unsigned int presentationContextId = 1; + for (size_t i = 0; i < proposed_.size(); i++) + { + assert(presentationContextId <= 255); + const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str(); + + const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_; + + std::vector<const char*> transferSyntaxes; + transferSyntaxes.reserve(source.size()); + + for (std::set<DicomTransferSyntax>::const_iterator + it = source.begin(); it != source.end(); ++it) + { + transferSyntaxes.push_back(GetTransferSyntaxUid(*it)); + } + + assert(!transferSyntaxes.empty()); + CheckConnecting(parameters, ASC_addPresentationContext( + params_, presentationContextId, abstractSyntax, + &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole)); + + presentationContextId += 2; + } + + // Do the association + CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_)); + isOpen_ = true; + + // Inspect the accepted transfer syntaxes + LST_HEAD **l = ¶ms_->DULparams.acceptedPresentationContext; + if (*l != NULL) + { + DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l); + LST_Position(l, (LST_NODE*)pc); + while (pc) + { + if (pc->result == ASC_P_ACCEPTANCE) + { + DicomTransferSyntax transferSyntax; + if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax)) + { + AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID); + } + else + { + LOG(WARNING) << "Unknown transfer syntax received from AET \"" + << parameters.GetRemoteApplicationEntityTitle() + << "\": " << pc->acceptedTransferSyntax; + } + } + + pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l); + } + } + + if (accepted_.empty()) + { + throw OrthancException(ErrorCode_NoPresentationContext, + "Unable to negotiate a presentation context with AET \"" + + parameters.GetRemoteApplicationEntityTitle() + "\""); + } + } + + void DicomAssociation::Close() + { + if (isOpen_) + { + CloseInternal(); + } + } + + + bool DicomAssociation::LookupAcceptedPresentationContext(std::map<DicomTransferSyntax, uint8_t>& target, + const std::string& abstractSyntax) const + { + if (!IsOpen()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened"); + } + + AcceptedPresentationContexts::const_iterator found = accepted_.find(abstractSyntax); + + if (found == accepted_.end()) + { + return false; + } + else + { + target = found->second; + return true; + } + } + + + void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax) + { + std::set<DicomTransferSyntax> ts; + ts.insert(DicomTransferSyntax_LittleEndianImplicit); + ts.insert(DicomTransferSyntax_LittleEndianExplicit); + ts.insert(DicomTransferSyntax_BigEndianExplicit); // Retired + ProposePresentationContext(abstractSyntax, ts); + } + + + void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax, + DicomTransferSyntax transferSyntax) + { + std::set<DicomTransferSyntax> ts; + ts.insert(transferSyntax); + ProposePresentationContext(abstractSyntax, ts); + } + + + size_t DicomAssociation::GetRemainingPropositions() const + { + assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS); + return MAX_PROPOSED_PRESENTATIONS - proposed_.size(); + } + + + void DicomAssociation::ProposePresentationContext( + const std::string& abstractSyntax, + const std::set<DicomTransferSyntax>& transferSyntaxes) + { + if (transferSyntaxes.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "No transfer syntax provided"); + } + + if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Too many proposed presentation contexts"); + } + + if (IsOpen()) + { + Close(); + } + + ProposedPresentationContext context; + context.abstractSyntax_ = abstractSyntax; + context.transferSyntaxes_ = transferSyntaxes; + + proposed_.push_back(context); + } + + + T_ASC_Association& DicomAssociation::GetDcmtkAssociation() const + { + if (isOpen_) + { + assert(assoc_ != NULL); + return *assoc_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The connection is not open"); + } + } + + + T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const + { + if (isOpen_) + { + assert(net_ != NULL); + return *net_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The connection is not open"); + } + } + + + void DicomAssociation::CheckCondition(const OFCondition& cond, + const DicomAssociationParameters& parameters, + const std::string& command) + { + if (cond.bad()) + { + // Reformat the error message from DCMTK by turning multiline + // errors into a single line + + std::string s(cond.text()); + std::string info; + info.reserve(s.size()); + + bool isMultiline = false; + for (size_t i = 0; i < s.size(); i++) + { + if (s[i] == '\r') + { + // Ignore + } + else if (s[i] == '\n') + { + if (isMultiline) + { + info += "; "; + } + else + { + info += " ("; + isMultiline = true; + } + } + else + { + info.push_back(s[i]); + } + } + + if (isMultiline) + { + info += ")"; + } + + throw OrthancException(ErrorCode_NetworkProtocol, + "DicomUserConnection - " + command + " to AET \"" + + parameters.GetRemoteApplicationEntityTitle() + + "\": " + info); + } + } + + + void DicomAssociation::ReportStorageCommitment( + const DicomAssociationParameters& parameters, + 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); + } + + + 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)); + } + } + } + + DicomAssociation association; + + { + std::set<DicomTransferSyntax> transferSyntaxes; + transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); + transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); + + association.SetRole(DicomAssociationRole_Scp); + association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, + transferSyntaxes); + } + + association.Open(parameters); + + /** + * 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 \"" + << parameters.GetRemoteApplicationEntityTitle() + << "\" about storage commitment transaction: " << transactionUid + << " (" << successSopClassUids.size() << " successes, " + << failedSopClassUids.size() << " failures)"; + const DIC_US messageId = association.GetDcmtkAssociation().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( + &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to send N-EVENT-REPORT request to AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + if (!DIMSE_sendMessageUsingMemoryData( + &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */, + &dataset, NULL /* callback */, NULL /* callback context */, + NULL /* commandSet */).good()) + { + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + /** + * Read the "EVENT_REPORT_RSP" response + **/ + + { + T_ASC_PresentationContextID presID = 0; + T_DIMSE_Message message; + + if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(), + (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + parameters.GetTimeout(), &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: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + 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: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + if (content.DimseStatus != 0 /* success */) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "The request cannot be handled by remote AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + } + + association.Close(); + } + + + void DicomAssociation::RequestStorageCommitment( + const DicomAssociationParameters& parameters, + 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); + } + + DicomAssociation association; + + { + std::set<DicomTransferSyntax> transferSyntaxes; + transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); + transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); + + association.SetRole(DicomAssociationRole_Default); + association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, + transferSyntaxes); + } + + association.Open(parameters); + + /** + * 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 \"" + << parameters.GetRemoteApplicationEntityTitle() + << "\" about storage commitment for " << sopClassUids.size() + << " instances, with transaction UID: " << transactionUid; + const DIC_US messageId = association.GetDcmtkAssociation().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( + &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to send N-ACTION request to AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + if (!DIMSE_sendMessageUsingMemoryData( + &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */, + &dataset, NULL /* callback */, NULL /* callback context */, + NULL /* commandSet */).good()) + { + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + /** + * Read the "N_ACTION_RSP" response + **/ + + { + T_ASC_PresentationContextID presID = 0; + T_DIMSE_Message message; + + if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(), + (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + parameters.GetTimeout(), &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: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + 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: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + if (content.DimseStatus != 0 /* success */) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "The request cannot be handled by remote AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + } + + association.Close(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomAssociation.h Fri Apr 10 16:04:54 2020 +0200 @@ -0,0 +1,143 @@ +/** + * 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 + +#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1 +# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1 +#endif + +#include "DicomAssociationParameters.h" + +#include <dcmtk/dcmnet/dimse.h> + +#include <stdint.h> // For uint8_t +#include <boost/noncopyable.hpp> +#include <set> + +namespace Orthanc +{ + class DicomAssociation : public boost::noncopyable + { + private: + // This is the maximum number of presentation context IDs (the + // number of odd integers between 1 and 255) + // http://dicom.nema.org/medical/dicom/2019e/output/chtml/part08/sect_9.3.2.2.html + static const size_t MAX_PROPOSED_PRESENTATIONS = 128; + + struct ProposedPresentationContext + { + std::string abstractSyntax_; + std::set<DicomTransferSyntax> transferSyntaxes_; + }; + + typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> > + AcceptedPresentationContexts; + + DicomAssociationRole role_; + bool isOpen_; + std::vector<ProposedPresentationContext> proposed_; + AcceptedPresentationContexts accepted_; + T_ASC_Network* net_; + T_ASC_Parameters* params_; + T_ASC_Association* assoc_; + + void Initialize(); + + void CheckConnecting(const DicomAssociationParameters& parameters, + const OFCondition& cond); + + void CloseInternal(); + + void AddAccepted(const std::string& abstractSyntax, + DicomTransferSyntax syntax, + uint8_t presentationContextId); + + public: + DicomAssociation() + { + Initialize(); + } + + ~DicomAssociation(); + + bool IsOpen() const + { + return isOpen_; + } + + void SetRole(DicomAssociationRole role); + + void ClearPresentationContexts(); + + void Open(const DicomAssociationParameters& parameters); + + void Close(); + + bool LookupAcceptedPresentationContext( + std::map<DicomTransferSyntax, uint8_t>& target, + const std::string& abstractSyntax) const; + + void ProposeGenericPresentationContext(const std::string& abstractSyntax); + + void ProposePresentationContext(const std::string& abstractSyntax, + DicomTransferSyntax transferSyntax); + + size_t GetRemainingPropositions() const; + + void ProposePresentationContext( + const std::string& abstractSyntax, + const std::set<DicomTransferSyntax>& transferSyntaxes); + + T_ASC_Association& GetDcmtkAssociation() const; + + T_ASC_Network& GetDcmtkNetwork() const; + + static void CheckCondition(const OFCondition& cond, + const DicomAssociationParameters& parameters, + const std::string& command); + + static void ReportStorageCommitment( + const DicomAssociationParameters& parameters, + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::vector<StorageCommitmentFailureReason>& failureReasons); + + static void RequestStorageCommitment( + const DicomAssociationParameters& parameters, + 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/DicomAssociationParameters.cpp Fri Apr 10 16:04:54 2020 +0200 @@ -0,0 +1,152 @@ +/** + * 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 "../PrecompiledHeaders.h" +#include "DicomAssociationParameters.h" + + +#ifdef _WIN32 +/** + * "The maximum length, in bytes, of the string returned in the buffer + * pointed to by the name parameter is dependent on the namespace provider, + * but this string must be 256 bytes or less. + * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx + **/ +# define HOST_NAME_MAX 256 +# include <winsock.h> +#endif + + +#if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX) +/** + * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that + * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an + * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect + * that the result will fit." + * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html + **/ +#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX +#endif + + +#include "../Logging.h" +#include "../OrthancException.h" + +#include <boost/thread/mutex.hpp> + +// By default, the timeout for client DICOM connections is set to 10 seconds +static boost::mutex defaultTimeoutMutex_; +static uint32_t defaultTimeout_ = 10; + + +namespace Orthanc +{ + void DicomAssociationParameters::ReadDefaultTimeout() + { + boost::mutex::scoped_lock lock(defaultTimeoutMutex_); + timeout_ = defaultTimeout_; + } + + + DicomAssociationParameters::DicomAssociationParameters() : + localAet_("STORESCU"), + remoteAet_("ANY-SCP"), + remoteHost_("127.0.0.1"), + remotePort_(104), + manufacturer_(ModalityManufacturer_Generic) + { + ReadDefaultTimeout(); + } + + + DicomAssociationParameters::DicomAssociationParameters(const std::string& localAet, + const RemoteModalityParameters& remote) : + localAet_(localAet), + remoteAet_(remote.GetApplicationEntityTitle()), + remoteHost_(remote.GetHost()), + remotePort_(remote.GetPortNumber()), + manufacturer_(remote.GetManufacturer()), + timeout_(defaultTimeout_) + { + ReadDefaultTimeout(); + } + + + void DicomAssociationParameters::SetRemoteHost(const std::string& host) + { + if (host.size() > HOST_NAME_MAX - 10) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Invalid host name (too long): " + host); + } + + remoteHost_ = host; + } + + + void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& parameters) + { + SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); + SetRemoteHost(parameters.GetHost()); + SetRemotePort(parameters.GetPortNumber()); + SetRemoteManufacturer(parameters.GetManufacturer()); + } + + + bool DicomAssociationParameters::IsEqual(const DicomAssociationParameters& other) const + { + return (localAet_ == other.localAet_ && + remoteAet_ == other.remoteAet_ && + remoteHost_ == other.remoteHost_ && + remotePort_ == other.remotePort_ && + manufacturer_ == other.manufacturer_); + } + + + void DicomAssociationParameters::SetDefaultTimeout(uint32_t seconds) + { + LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " + << seconds << " seconds (0 = no timeout)"; + + { + boost::mutex::scoped_lock lock(defaultTimeoutMutex_); + defaultTimeout_ = seconds; + } + } + + + size_t DicomAssociationParameters::GetMaxHostNameSize() + { + return HOST_NAME_MAX; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomAssociationParameters.h Fri Apr 10 16:04:54 2020 +0200 @@ -0,0 +1,130 @@ +/** + * 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 "RemoteModalityParameters.h" + +class OFCondition; // From DCMTK + +namespace Orthanc +{ + class DicomAssociationParameters + { + private: + std::string localAet_; + std::string remoteAet_; + std::string remoteHost_; + uint16_t remotePort_; + ModalityManufacturer manufacturer_; + uint32_t timeout_; + + void ReadDefaultTimeout(); + + public: + DicomAssociationParameters(); + + DicomAssociationParameters(const std::string& localAet, + const RemoteModalityParameters& remote); + + const std::string& GetLocalApplicationEntityTitle() const + { + return localAet_; + } + + const std::string& GetRemoteApplicationEntityTitle() const + { + return remoteAet_; + } + + const std::string& GetRemoteHost() const + { + return remoteHost_; + } + + uint16_t GetRemotePort() const + { + return remotePort_; + } + + ModalityManufacturer GetRemoteManufacturer() const + { + return manufacturer_; + } + + void SetLocalApplicationEntityTitle(const std::string& aet) + { + localAet_ = aet; + } + + void SetRemoteApplicationEntityTitle(const std::string& aet) + { + remoteAet_ = aet; + } + + void SetRemoteHost(const std::string& host); + + void SetRemotePort(uint16_t port) + { + remotePort_ = port; + } + + void SetRemoteManufacturer(ModalityManufacturer manufacturer) + { + manufacturer_ = manufacturer; + } + + void SetRemoteModality(const RemoteModalityParameters& parameters); + + bool IsEqual(const DicomAssociationParameters& other) const; + + void SetTimeout(uint32_t seconds) + { + timeout_ = seconds; + } + + uint32_t GetTimeout() const + { + return timeout_; + } + + bool HasTimeout() const + { + return timeout_ != 0; + } + + static void SetDefaultTimeout(uint32_t seconds); + + static size_t GetMaxHostNameSize(); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomControlUserConnection.cpp Fri Apr 10 16:04:54 2020 +0200 @@ -0,0 +1,658 @@ +/** + * 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 "../PrecompiledHeaders.h" +#include "DicomControlUserConnection.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../DicomParsing/FromDcmtkBridge.h" + +#include <dcmtk/dcmdata/dcdeftag.h> +#include <dcmtk/dcmnet/diutil.h> + +namespace Orthanc +{ + static void TestAndCopyTag(DicomMap& result, + const DicomMap& source, + const DicomTag& tag) + { + if (!source.HasTag(tag)) + { + throw OrthancException(ErrorCode_BadRequest); + } + else + { + result.SetValue(tag, source.GetValue(tag)); + } + } + + + namespace + { + struct FindPayload + { + DicomFindAnswers* answers; + const char* level; + bool isWorklist; + }; + } + + + static void FindCallback( + /* in */ + void *callbackData, + T_DIMSE_C_FindRQ *request, /* original find request */ + int responseCount, + T_DIMSE_C_FindRSP *response, /* pending response received */ + DcmDataset *responseIdentifiers /* pending response identifiers */ + ) + { + FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData); + + if (responseIdentifiers != NULL) + { + if (payload.isWorklist) + { + ParsedDicomFile answer(*responseIdentifiers); + payload.answers->Add(answer); + } + else + { + DicomMap m; + FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers); + + if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) + { + m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false); + } + + payload.answers->Add(m); + } + } + } + + + static void NormalizeFindQuery(DicomMap& fixedQuery, + ResourceType level, + const DicomMap& fields) + { + std::set<DicomTag> allowedTags; + + // WARNING: Do not add "break" or reorder items in this switch-case! + switch (level) + { + case ResourceType_Instance: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance); + + case ResourceType_Series: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Series); + + case ResourceType_Study: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Study); + + case ResourceType_Patient: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + switch (level) + { + case ResourceType_Patient: + allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); + allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); + allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); + break; + + case ResourceType_Study: + allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); + allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); + allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); + allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY); + break; + + case ResourceType_Series: + allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); + break; + + default: + break; + } + + allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET); + + DicomArray query(fields); + for (size_t i = 0; i < query.GetSize(); i++) + { + const DicomTag& tag = query.GetElement(i).GetTag(); + if (allowedTags.find(tag) == allowedTags.end()) + { + LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag; + } + else + { + fixedQuery.SetValue(tag, query.GetElement(i).GetValue()); + } + } + } + + + + static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields, + ModalityManufacturer manufacturer) + { + // Fix outgoing C-Find requests issue for Syngo.Via and its + // solution was reported by Emsy Chan by private mail on + // 2015-06-17. According to Robert van Ommen (2015-11-30), the + // same fix is required for Agfa Impax. This was generalized for + // generic manufacturer since it seems to affect PhilipsADW, + // GEWAServer as well: + // https://bitbucket.org/sjodogne/orthanc/issues/31/ + + switch (manufacturer) + { + case ModalityManufacturer_GenericNoWildcardInDates: + case ModalityManufacturer_GenericNoUniversalWildcard: + { + std::unique_ptr<DicomMap> fix(fields.Clone()); + + std::set<DicomTag> tags; + fix->GetTags(tags); + + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + // Replace a "*" wildcard query by an empty query ("") for + // "date" or "all" value representations depending on the + // type of manufacturer. + if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard || + (manufacturer == ModalityManufacturer_GenericNoWildcardInDates && + FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date)) + { + const DicomValue* value = fix->TestAndGetValue(*it); + + if (value != NULL && + !value->IsNull() && + value->GetContent() == "*") + { + fix->SetValue(*it, "", false); + } + } + } + + return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */); + } + + default: + return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */); + } + } + + + + void DicomControlUserConnection::SetupPresentationContexts() + { + association_.ProposeGenericPresentationContext(UID_VerificationSOPClass); + association_.ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel); + association_.ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel); + association_.ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel); + association_.ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel); + } + + + void DicomControlUserConnection::FindInternal(DicomFindAnswers& answers, + DcmDataset* dataset, + const char* sopClass, + bool isWorklist, + const char* level) + { + assert(isWorklist ^ (level != NULL)); + + association_.Open(parameters_); + + FindPayload payload; + payload.answers = &answers; + payload.level = level; + payload.isWorklist = isWorklist; + + // Figure out which of the accepted presentation contexts should be used + int presID = ASC_findAcceptedPresentationContextID( + &association_.GetDcmtkAssociation(), sopClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_DicomFindUnavailable, + "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle()); + } + + T_DIMSE_C_FindRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = association_.GetDcmtkAssociation().nextMsgID++; + strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); + request.Priority = DIMSE_PRIORITY_MEDIUM; + request.DataSetType = DIMSE_DATASET_PRESENT; + + T_DIMSE_C_FindRSP response; + DcmDataset* statusDetail = NULL; + +#if DCMTK_VERSION_NUMBER >= 364 + int responseCount; +#endif + + OFCondition cond = DIMSE_findUser( + &association_.GetDcmtkAssociation(), presID, &request, dataset, +#if DCMTK_VERSION_NUMBER >= 364 + responseCount, +#endif + FindCallback, &payload, + /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + /*opt_dimse_timeout*/ parameters_.GetTimeout(), + &response, &statusDetail); + + if (statusDetail) + { + delete statusDetail; + } + + DicomAssociation::CheckCondition(cond, parameters_, "C-FIND"); + + + /** + * New in Orthanc 1.6.0: Deal with failures during C-FIND. + * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1 + **/ + + if (response.DimseStatus != 0x0000 && // Success + response.DimseStatus != 0xFF00 && // Pending - Matches are continuing + response.DimseStatus != 0xFF01) // Pending - Matches are continuing + { + char buf[16]; + sprintf(buf, "%04X", response.DimseStatus); + + if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess) + { + throw OrthancException(ErrorCode_NetworkProtocol, + HttpStatus_422_UnprocessableEntity, + "C-FIND SCU to AET \"" + + parameters_.GetRemoteApplicationEntityTitle() + + "\" has failed with DIMSE status 0x" + buf + + " (unable to process - invalid query ?)"); + } + else + { + throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" + + parameters_.GetRemoteApplicationEntityTitle() + + "\" has failed with DIMSE status 0x" + buf); + } + } + } + + + void DicomControlUserConnection::MoveInternal(const std::string& targetAet, + ResourceType level, + const DicomMap& fields) + { + association_.Open(parameters_); + + std::unique_ptr<ParsedDicomFile> query( + ConvertQueryFields(fields, parameters_.GetRemoteManufacturer())); + DcmDataset* dataset = query->GetDcmtkObject().getDataset(); + + const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; + switch (level) + { + case ResourceType_Patient: + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); + break; + + case ResourceType_Study: + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); + break; + + case ResourceType_Series: + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); + break; + + case ResourceType_Instance: + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // Figure out which of the accepted presentation contexts should be used + int presID = ASC_findAcceptedPresentationContextID(&association_.GetDcmtkAssociation(), sopClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_DicomMoveUnavailable, + "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle()); + } + + T_DIMSE_C_MoveRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = association_.GetDcmtkAssociation().nextMsgID++; + strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); + request.Priority = DIMSE_PRIORITY_MEDIUM; + request.DataSetType = DIMSE_DATASET_PRESENT; + strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN); + + T_DIMSE_C_MoveRSP response; + DcmDataset* statusDetail = NULL; + DcmDataset* responseIdentifiers = NULL; + OFCondition cond = DIMSE_moveUser( + &association_.GetDcmtkAssociation(), presID, &request, dataset, NULL, NULL, + /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + /*opt_dimse_timeout*/ parameters_.GetTimeout(), + &association_.GetDcmtkNetwork(), NULL, NULL, + &response, &statusDetail, &responseIdentifiers); + + if (statusDetail) + { + delete statusDetail; + } + + if (responseIdentifiers) + { + delete responseIdentifiers; + } + + DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE"); + + + /** + * New in Orthanc 1.6.0: Deal with failures during C-MOVE. + * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2 + **/ + + if (response.DimseStatus != 0x0000 && // Success + response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing + { + char buf[16]; + sprintf(buf, "%04X", response.DimseStatus); + + if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess) + { + throw OrthancException(ErrorCode_NetworkProtocol, + HttpStatus_422_UnprocessableEntity, + "C-MOVE SCU to AET \"" + + parameters_.GetRemoteApplicationEntityTitle() + + "\" has failed with DIMSE status 0x" + buf + + " (unable to process - resource not found ?)"); + } + else + { + throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" + + parameters_.GetRemoteApplicationEntityTitle() + + "\" has failed with DIMSE status 0x" + buf); + } + } + } + + + DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) : + parameters_(params) + { + SetupPresentationContexts(); + } + + + bool DicomControlUserConnection::Echo() + { + association_.Open(parameters_); + + DIC_US status; + DicomAssociation::CheckCondition( + DIMSE_echoUser(&association_.GetDcmtkAssociation(), + association_.GetDcmtkAssociation().nextMsgID++, + /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + /*opt_dimse_timeout*/ parameters_.GetTimeout(), + &status, NULL), + parameters_, "C-ECHO"); + + return status == STATUS_Success; + } + + + void DicomControlUserConnection::Find(DicomFindAnswers& result, + ResourceType level, + const DicomMap& originalFields, + bool normalize) + { + std::unique_ptr<ParsedDicomFile> query; + + if (normalize) + { + DicomMap fields; + NormalizeFindQuery(fields, level, originalFields); + query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer())); + } + else + { + query.reset(new ParsedDicomFile(originalFields, + GetDefaultDicomEncoding(), + false /* be strict */)); + } + + DcmDataset* dataset = query->GetDcmtkObject().getDataset(); + + const char* clevel = NULL; + const char* sopClass = NULL; + + switch (level) + { + case ResourceType_Patient: + clevel = "PATIENT"; + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); + sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; + break; + + case ResourceType_Study: + clevel = "STUDY"; + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + + case ResourceType_Series: + clevel = "SERIES"; + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + + case ResourceType_Instance: + clevel = "IMAGE"; + DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + + const char* universal; + if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE) + { + universal = "*"; + } + else + { + universal = ""; + } + + + // Add the expected tags for this query level. + // WARNING: Do not reorder or add "break" in this switch-case! + switch (level) + { + case ResourceType_Instance: + if (!dataset->tagExists(DCM_SOPInstanceUID)) + { + DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal); + } + + case ResourceType_Series: + if (!dataset->tagExists(DCM_SeriesInstanceUID)) + { + DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal); + } + + case ResourceType_Study: + if (!dataset->tagExists(DCM_AccessionNumber)) + { + DU_putStringDOElement(dataset, DCM_AccessionNumber, universal); + } + + if (!dataset->tagExists(DCM_StudyInstanceUID)) + { + DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal); + } + + case ResourceType_Patient: + if (!dataset->tagExists(DCM_PatientID)) + { + DU_putStringDOElement(dataset, DCM_PatientID, universal); + } + + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + assert(clevel != NULL && sopClass != NULL); + FindInternal(result, dataset, sopClass, false, clevel); + } + + + void DicomControlUserConnection::Move(const std::string& targetAet, + ResourceType level, + const DicomMap& findResult) + { + DicomMap move; + switch (level) + { + case ResourceType_Patient: + TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID); + break; + + case ResourceType_Study: + TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + break; + + case ResourceType_Series: + TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); + break; + + case ResourceType_Instance: + TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); + TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + MoveInternal(targetAet, level, move); + } + + + void DicomControlUserConnection::Move(const std::string& targetAet, + const DicomMap& findResult) + { + if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) + { + throw OrthancException(ErrorCode_InternalError); + } + + const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); + ResourceType level = StringToResourceType(tmp.c_str()); + + Move(targetAet, level, findResult); + } + + + void DicomControlUserConnection::MovePatient(const std::string& targetAet, + const std::string& patientId) + { + DicomMap query; + query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false); + MoveInternal(targetAet, ResourceType_Patient, query); + } + + + void DicomControlUserConnection::MoveStudy(const std::string& targetAet, + const std::string& studyUid) + { + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); + MoveInternal(targetAet, ResourceType_Study, query); + } + + + void DicomControlUserConnection::MoveSeries(const std::string& targetAet, + const std::string& studyUid, + const std::string& seriesUid) + { + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); + query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); + MoveInternal(targetAet, ResourceType_Series, query); + } + + + void DicomControlUserConnection::MoveInstance(const std::string& targetAet, + const std::string& studyUid, + const std::string& seriesUid, + const std::string& instanceUid) + { + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); + query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); + query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false); + MoveInternal(targetAet, ResourceType_Instance, query); + } + + + void DicomControlUserConnection::FindWorklist(DicomFindAnswers& result, + ParsedDicomFile& query) + { + DcmDataset* dataset = query.GetDcmtkObject().getDataset(); + const char* sopClass = UID_FINDModalityWorklistInformationModel; + + FindInternal(result, dataset, sopClass, true, NULL); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomControlUserConnection.h Fri Apr 10 16:04:54 2020 +0200 @@ -0,0 +1,103 @@ +/** + * 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 + +#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1 +# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1 +#endif + +#include "DicomAssociation.h" +#include "DicomFindAnswers.h" + +namespace Orthanc +{ + class DicomControlUserConnection : public boost::noncopyable + { + private: + DicomAssociationParameters parameters_; + DicomAssociation association_; + + void SetupPresentationContexts(); + + void FindInternal(DicomFindAnswers& answers, + DcmDataset* dataset, + const char* sopClass, + bool isWorklist, + const char* level); + + void MoveInternal(const std::string& targetAet, + ResourceType level, + const DicomMap& fields); + + public: + DicomControlUserConnection(const DicomAssociationParameters& params); + + const DicomAssociationParameters& GetParameters() const + { + return parameters_; + } + + bool Echo(); + + void Find(DicomFindAnswers& result, + ResourceType level, + const DicomMap& originalFields, + bool normalize); + + void Move(const std::string& targetAet, + ResourceType level, + const DicomMap& findResult); + + void Move(const std::string& targetAet, + const DicomMap& findResult); + + void MovePatient(const std::string& targetAet, + const std::string& patientId); + + void MoveStudy(const std::string& targetAet, + const std::string& studyUid); + + void MoveSeries(const std::string& targetAet, + const std::string& studyUid, + const std::string& seriesUid); + + void MoveInstance(const std::string& targetAet, + const std::string& studyUid, + const std::string& seriesUid, + const std::string& instanceUid); + + void FindWorklist(DicomFindAnswers& result, + ParsedDicomFile& query); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomStoreUserConnection.cpp Fri Apr 10 16:04:54 2020 +0200 @@ -0,0 +1,237 @@ +/** + * 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 "../PrecompiledHeaders.h" +#include "DicomStoreUserConnection.h" + +#include "../Logging.h" +#include "../OrthancException.h" + +namespace Orthanc +{ + bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid, + const std::set<DicomTransferSyntax>& syntaxes) + { + size_t requiredCount = syntaxes.size(); + if (proposeUncompressedSyntaxes_) + { + requiredCount += 1; + } + + if (association_.GetRemainingPropositions() <= requiredCount) + { + return false; // Not enough room + } + + for (std::set<DicomTransferSyntax>::const_iterator + it = syntaxes.begin(); it != syntaxes.end(); ++it) + { + association_.ProposePresentationContext(sopClassUid, *it); + } + + if (proposeUncompressedSyntaxes_) + { + std::set<DicomTransferSyntax> uncompressed; + + if (syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end()) + { + uncompressed.insert(DicomTransferSyntax_LittleEndianImplicit); + } + + if (syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end()) + { + uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit); + } + + if (proposeRetiredBigEndian_ && + syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end()) + { + uncompressed.insert(DicomTransferSyntax_BigEndianExplicit); + } + + if (!uncompressed.empty()) + { + association_.ProposePresentationContext(sopClassUid, uncompressed); + } + } + + return true; + } + + + bool DicomStoreUserConnection::LookupPresentationContext( + uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax) + { + typedef std::map<DicomTransferSyntax, uint8_t> PresentationContexts; + + PresentationContexts pc; + if (association_.IsOpen() && + association_.LookupAcceptedPresentationContext(pc, sopClassUid)) + { + PresentationContexts::const_iterator found = pc.find(transferSyntax); + if (found != pc.end()) + { + presentationContextId = found->second; + return true; + } + } + + return false; + } + + + DicomStoreUserConnection::DicomStoreUserConnection( + const DicomAssociationParameters& params) : + parameters_(params), + proposeCommonClasses_(true), + proposeUncompressedSyntaxes_(true), + proposeRetiredBigEndian_(false) + { + } + + + void DicomStoreUserConnection::PrepareStorageClass(const std::string& sopClassUid, + DicomTransferSyntax syntax) + { + StorageClasses::iterator found = storageClasses_.find(sopClassUid); + + if (found == storageClasses_.end()) + { + std::set<DicomTransferSyntax> ts; + ts.insert(syntax); + storageClasses_[sopClassUid] = ts; + } + else + { + found->second.insert(syntax); + } + } + + + bool DicomStoreUserConnection::NegotiatePresentationContext( + uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax) + { + /** + * Step 1: Check whether this presentation context is already + * available in the previously negociated assocation. + **/ + + if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax)) + { + return true; + } + + // The association must be re-negotiated + LOG(INFO) << "Re-negociating DICOM association with " + << parameters_.GetRemoteApplicationEntityTitle(); + association_.ClearPresentationContexts(); + PrepareStorageClass(sopClassUid, transferSyntax); + + + /** + * Step 2: Propose at least the mandatory SOP class. + **/ + + { + StorageClasses::const_iterator mandatory = storageClasses_.find(sopClassUid); + + if (mandatory == storageClasses_.end() || + mandatory->second.find(transferSyntax) == mandatory->second.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (!ProposeStorageClass(sopClassUid, mandatory->second)) + { + // Should never happen in real life: There are no more than + // 128 transfer syntaxes in DICOM! + throw OrthancException(ErrorCode_InternalError, + "Too many transfer syntaxes for SOP class UID: " + sopClassUid); + } + } + + + /** + * Step 3: Propose all the previously spotted SOP classes, as + * registered through the "PrepareStorageClass()" method. + **/ + + for (StorageClasses::const_iterator it = storageClasses_.begin(); + it != storageClasses_.end(); ++it) + { + if (it->first != sopClassUid) + { + ProposeStorageClass(it->first, it->second); + } + } + + + /** + * Step 4: As long as there is room left in the proposed + * presentation contexts, propose the uncompressed transfer syntaxes + * for the most common SOP classes, as can be found in the + * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The + * preferred transfer syntax is "LittleEndianImplicit". + **/ + + if (proposeCommonClasses_) + { + std::set<DicomTransferSyntax> ts; + ts.insert(DicomTransferSyntax_LittleEndianImplicit); + + for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++) + { + std::string c(dcmShortSCUStorageSOPClassUIDs[i]); + + if (c != sopClassUid && + storageClasses_.find(c) == storageClasses_.end()) + { + ProposeStorageClass(c, ts); + } + } + } + + + /** + * Step 5: Open the association, and check whether the pair (SOP + * class UID, transfer syntax) was accepted by the remote host. + **/ + + association_.Open(parameters_); + return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomStoreUserConnection.h Fri Apr 10 16:04:54 2020 +0200 @@ -0,0 +1,126 @@ +/** + * 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 "DicomAssociation.h" + + +namespace Orthanc +{ + /** + + Orthanc < 1.7.0: + + Input | Output + -------------+--------------------------------------------- + Compressed | Same transfer syntax + Uncompressed | Same transfer syntax, or other uncompressed + + Orthanc >= 1.7.0: + + Input | Output + -------------+--------------------------------------------- + Compressed | Same transfer syntax, or uncompressed + Uncompressed | Same transfer syntax, or other uncompressed + + **/ + + + class DicomStoreUserConnection : public boost::noncopyable + { + private: + typedef std::map<std::string, std::set<DicomTransferSyntax> > StorageClasses; + + DicomAssociationParameters parameters_; + DicomAssociation association_; + StorageClasses storageClasses_; + bool proposeCommonClasses_; + bool proposeUncompressedSyntaxes_; + bool proposeRetiredBigEndian_; + + + // Return "false" if there is not enough room remaining in the association + bool ProposeStorageClass(const std::string& sopClassUid, + const std::set<DicomTransferSyntax>& syntaxes); + + bool LookupPresentationContext(uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax); + + public: + DicomStoreUserConnection(const DicomAssociationParameters& params); + + const DicomAssociationParameters& GetParameters() const + { + return parameters_; + } + + void SetCommonClassesProposed(bool proposed) + { + proposeCommonClasses_ = proposed; + } + + bool IsCommonClassesProposed() const + { + return proposeCommonClasses_; + } + + void SetUncompressedSyntaxesProposed(bool proposed) + { + proposeUncompressedSyntaxes_ = proposed; + } + + bool IsUncompressedSyntaxesProposed() const + { + return proposeUncompressedSyntaxes_; + } + + void SetRetiredBigEndianProposed(bool propose) + { + proposeRetiredBigEndian_ = propose; + } + + bool IsRetiredBigEndianProposed() const + { + return proposeRetiredBigEndian_; + } + + void PrepareStorageClass(const std::string& sopClassUid, + DicomTransferSyntax syntax); + + bool NegotiatePresentationContext(uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax); + }; +}
--- a/Core/Enumerations.h Wed Apr 08 14:50:06 2020 +0200 +++ b/Core/Enumerations.h Fri Apr 10 16:04:54 2020 +0200 @@ -747,6 +747,14 @@ }; + enum DicomAssociationRole + { + DicomAssociationRole_Default, + DicomAssociationRole_Scu, + DicomAssociationRole_Scp + }; + + /** * WARNING: Do not change the explicit values in the enumerations * below this point. This would result in incompatible databases
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed Apr 08 14:50:06 2020 +0200 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Apr 10 16:04:54 2020 +0200 @@ -481,6 +481,10 @@ if (ENABLE_DCMTK_NETWORKING) add_definitions(-DORTHANC_ENABLE_DCMTK_NETWORKING=1) list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL + ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociation.cpp + ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociationParameters.cpp + ${ORTHANC_ROOT}/Core/DicomNetworking/DicomControlUserConnection.cpp + ${ORTHANC_ROOT}/Core/DicomNetworking/DicomStoreUserConnection.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomFindAnswers.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomServer.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomUserConnection.cpp