Mercurial > hg > orthanc
changeset 3827:638906dcfe32 transcoding
integration mainline->transcoding
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 10 Apr 2020 16:18:17 +0200 |
parents | 6762506ef4fb (current diff) e82bd07c384e (diff) |
children | 4fde7933e504 |
files | Core/DicomNetworking/DicomStoreUserConnection.h Core/Enumerations.h UnitTestsSources/FromDcmtkTests.cpp |
diffstat | 12 files changed, 2475 insertions(+), 1929 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:18:17 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:18:17 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:18:17 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:18:17 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:18:17 2020 +0200 @@ -0,0 +1,660 @@ +/** + * 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 "../DicomParsing/FromDcmtkBridge.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "DicomAssociation.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), + association_(new DicomAssociation) + { + 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:18:17 2020 +0200 @@ -0,0 +1,107 @@ +/** + * 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 "DicomFindAnswers.h" + +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class DicomAssociation; // Forward declaration for PImpl design pattern + + class DicomControlUserConnection : public boost::noncopyable + { + private: + DicomAssociationParameters parameters_; + boost::shared_ptr<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:18:17 2020 +0200 @@ -0,0 +1,240 @@ +/** + * 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 "DicomAssociation.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), + association_(new DicomAssociation), + 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:18:17 2020 +0200 @@ -0,0 +1,131 @@ +/** + * 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 "DicomAssociationParameters.h" + +#include <boost/shared_ptr.hpp> +#include <boost/noncopyable.hpp> +#include <set> +#include <stdint.h> // For uint8_t + + +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 DicomAssociation; // Forward declaration for PImpl design pattern + + class DicomStoreUserConnection : public boost::noncopyable + { + private: + typedef std::map<std::string, std::set<DicomTransferSyntax> > StorageClasses; + + DicomAssociationParameters parameters_; + boost::shared_ptr<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); + + 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 LookupPresentationContext(uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax); + + bool NegotiatePresentationContext(uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax); + }; +}
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Fri Apr 10 15:24:02 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Fri Apr 10 16:18:17 2020 +0200 @@ -694,8 +694,8 @@ dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT); } - windowWidth = static_cast<float>(1 << info.GetBitsStored()); - windowCenter = windowWidth / 2.0f; + windowWidth = static_cast<float>(1 << info.GetBitsStored()) * rescaleSlope; + windowCenter = windowWidth / 2.0f + rescaleIntercept; if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) && dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH))
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Apr 10 15:24:02 2020 +0200 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Apr 10 16:18:17 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
--- a/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py Fri Apr 10 15:24:02 2020 +0200 +++ b/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py Fri Apr 10 16:18:17 2020 +0200 @@ -72,7 +72,10 @@ # Authentication (for some weird reason, this method does # not always work) # http://en.wikipedia.org/wiki/Basic_access_authentication - headers['authorization'] = 'Basic ' + base64.b64encode(username + ':' + password) + creds_str = username + ':' + password + creds_str_bytes = creds_str.encode("ascii") + creds_str_bytes_b64 = b'Basic ' + base64.b64encode(creds_str_bytes) + headers['authorization'] = creds_str_bytes_b64.decode("ascii") resp, content = h.request(URL, 'POST', body = content, @@ -85,6 +88,8 @@ sys.stdout.write(" => failure (Is it a DICOM file? Is there a password?)\n") except: + type, value, traceback = sys.exc_info() + sys.stderr.write(str(value)) sys.stdout.write(" => unable to connect (Is Orthanc running? Is there a password?)\n")
--- a/UnitTestsSources/FromDcmtkTests.cpp Fri Apr 10 15:24:02 2020 +0200 +++ b/UnitTestsSources/FromDcmtkTests.cpp Fri Apr 10 16:18:17 2020 +0200 @@ -2406,1930 +2406,9 @@ -#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 "../Core/DicomNetworking/RemoteModalityParameters.h" - - -#include <dcmtk/dcmnet/diutil.h> // For dcmConnectionTimeout() - - - -namespace Orthanc -{ - // By default, the timeout for client DICOM connections is set to 10 seconds - static boost::mutex defaultTimeoutMutex_; - static uint32_t defaultTimeout_ = 10; - - - class DicomAssociationParameters - { - private: - std::string localAet_; - std::string remoteAet_; - std::string remoteHost_; - uint16_t remotePort_; - ModalityManufacturer manufacturer_; - uint32_t timeout_; - - void ReadDefaultTimeout() - { - boost::mutex::scoped_lock lock(defaultTimeoutMutex_); - timeout_ = defaultTimeout_; - } - - public: - DicomAssociationParameters() : - localAet_("STORESCU"), - remoteAet_("ANY-SCP"), - remoteHost_("127.0.0.1"), - remotePort_(104), - manufacturer_(ModalityManufacturer_Generic) - { - ReadDefaultTimeout(); - } - - 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(); - } - - 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) - { - if (host.size() > HOST_NAME_MAX - 10) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Invalid host name (too long): " + host); - } - - remoteHost_ = host; - } - - void SetRemotePort(uint16_t port) - { - remotePort_ = port; - } - - void SetRemoteManufacturer(ModalityManufacturer manufacturer) - { - manufacturer_ = manufacturer; - } - - void SetRemoteModality(const RemoteModalityParameters& parameters) - { - SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); - SetRemoteHost(parameters.GetHost()); - SetRemotePort(parameters.GetPortNumber()); - SetRemoteManufacturer(parameters.GetManufacturer()); - } - - bool IsEqual(const DicomAssociationParameters& other) const - { - return (localAet_ == other.localAet_ && - remoteAet_ == other.remoteAet_ && - remoteHost_ == other.remoteHost_ && - remotePort_ == other.remotePort_ && - manufacturer_ == other.manufacturer_); - } - - 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) - { - 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; - } - } - }; - - - 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); - } - } - } - } - - - 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() - { - role_ = DicomAssociationRole_Default; - isOpen_ = false; - net_ = NULL; - params_ = NULL; - assoc_ = NULL; - - // Must be after "isOpen_ = false" - ClearPresentationContexts(); - } - - void CheckConnecting(const DicomAssociationParameters& parameters, - const OFCondition& cond) - { - try - { - CheckCondition(cond, parameters, "connecting"); - } - catch (OrthancException&) - { - CloseInternal(); - throw; - } - } - - void 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 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; - } - } - } - - public: - DicomAssociation() - { - Initialize(); - } - - ~DicomAssociation() - { - try - { - Close(); - } - catch (OrthancException&) - { - // Don't throw exception in destructors - } - } - - bool IsOpen() const - { - return isOpen_; - } - - void SetRole(DicomAssociationRole role) - { - if (role_ != role) - { - Close(); - role_ = role; - } - } - - void ClearPresentationContexts() - { - Close(); - proposed_.clear(); - proposed_.reserve(MAX_PROPOSED_PRESENTATIONS); - } - - void 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 Close() - { - if (isOpen_) - { - CloseInternal(); - } - } - - bool 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 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 ProposePresentationContext(const std::string& abstractSyntax, - DicomTransferSyntax transferSyntax) - { - std::set<DicomTransferSyntax> ts; - ts.insert(transferSyntax); - ProposePresentationContext(abstractSyntax, ts); - } - - size_t GetRemainingPropositions() const - { - assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS); - return MAX_PROPOSED_PRESENTATIONS - proposed_.size(); - } - - void 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& GetDcmtkAssociation() const - { - if (isOpen_) - { - assert(assoc_ != NULL); - return *assoc_; - } - else - { - throw OrthancException(ErrorCode_BadSequenceOfCalls, - "The connection is not open"); - } - } - - T_ASC_Network& GetDcmtkNetwork() const - { - if (isOpen_) - { - assert(net_ != NULL); - return *net_; - } - else - { - throw OrthancException(ErrorCode_BadSequenceOfCalls, - "The connection is not open"); - } - } - - static void 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); - } - } - - - 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) - { - 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(); - } - - static void 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(); - } - }; - - - - 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 */); - } - } - - - - class DicomControlUserConnection : public boost::noncopyable - { - private: - DicomAssociationParameters parameters_; - DicomAssociation association_; - - void SetupPresentationContexts() - { - association_.ProposeGenericPresentationContext(UID_VerificationSOPClass); - association_.ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel); - association_.ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel); - association_.ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel); - association_.ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel); - } - - void 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 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); - } - } - } - - public: - DicomControlUserConnection(const DicomAssociationParameters& params) : - parameters_(params) - { - SetupPresentationContexts(); - } - - const DicomAssociationParameters& GetParameters() const - { - return parameters_; - } - - bool 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 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 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 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 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 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 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 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 FindWorklist(DicomFindAnswers& result, - ParsedDicomFile& query) - { - DcmDataset* dataset = query.GetDcmtkObject().getDataset(); - const char* sopClass = UID_FINDModalityWorklistInformationModel; - - FindInternal(result, dataset, sopClass, true, NULL); - } - }; - - - 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_; - - - /** - - 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 - - **/ - - - // Return "false" if there is not enough room remaining in the association - bool 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 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; - } - - - bool 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 - 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); - } - - public: - DicomStoreUserConnection(const DicomAssociationParameters& params) : - parameters_(params), - proposeCommonClasses_(true), - proposeUncompressedSyntaxes_(true), - proposeRetiredBigEndian_(false) - { - } - - 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) - { - 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); - } - } - - - void Toto(const std::string& sopClassUid, - DicomTransferSyntax transferSyntax) - { - uint8_t id; - - if (NegotiatePresentationContext(id, sopClassUid, transferSyntax)) - { - printf("**** OK, without transcoding !! %d\n", id); - } - else - { - // Transcoding - only in Orthanc >= 1.7.0 - - const DicomTransferSyntax uncompressed[] = { - DicomTransferSyntax_LittleEndianImplicit, // Default transfer syntax - DicomTransferSyntax_LittleEndianExplicit, - DicomTransferSyntax_BigEndianExplicit - }; - - bool found = false; - for (size_t i = 0; i < 3; i++) - { - if (LookupPresentationContext(id, sopClassUid, uncompressed[i])) - { - printf("**** TRANSCODING to %s => %d\n", - GetTransferSyntaxUid(uncompressed[i]), id); - found = true; - break; - } - } - - if (!found) - { - printf("**** KO KO KO\n"); - } - } - } - }; -} - +#include "../Core/DicomNetworking/DicomAssociation.h" +#include "../Core/DicomNetworking/DicomControlUserConnection.h" +#include "../Core/DicomNetworking/DicomStoreUserConnection.h" TEST(Toto, DISABLED_DicomAssociation) { @@ -4386,6 +2465,45 @@ #endif } +static void TestTranscode(DicomStoreUserConnection& scu, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax) +{ + uint8_t id; + + if (scu.NegotiatePresentationContext(id, sopClassUid, transferSyntax)) + { + printf("**** OK, without transcoding !! %d\n", id); + } + else + { + // Transcoding - only in Orthanc >= 1.7.0 + + const DicomTransferSyntax uncompressed[] = { + DicomTransferSyntax_LittleEndianImplicit, // Default transfer syntax + DicomTransferSyntax_LittleEndianExplicit, + DicomTransferSyntax_BigEndianExplicit + }; + + bool found = false; + for (size_t i = 0; i < 3; i++) + { + if (scu.LookupPresentationContext(id, sopClassUid, uncompressed[i])) + { + printf("**** TRANSCODING to %s => %d\n", + GetTransferSyntaxUid(uncompressed[i]), id); + found = true; + break; + } + } + + if (!found) + { + printf("**** KO KO KO\n"); + } + } +} + TEST(Toto, DISABLED_Store) { @@ -4401,8 +2519,8 @@ //assoc.SetUncompressedSyntaxesProposed(false); //assoc.SetCommonClassesProposed(false); - assoc.Toto(UID_MRImageStorage, DicomTransferSyntax_JPEG2000); - //assoc.Toto(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); + TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000); + //TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); } #endif