# HG changeset patch # User Sebastien Jodogne # Date 1586447125 -7200 # Node ID f89eac983c9b8618600dc3e0963d3597a6e143df # Parent 1237bd0bbdb290d690e383cc1a0c1823b25d1228 refactoring DicomUserConnection as DicomAssociation diff -r 1237bd0bbdb2 -r f89eac983c9b Core/Enumerations.h --- a/Core/Enumerations.h Wed Apr 08 17:00:33 2020 +0200 +++ b/Core/Enumerations.h Thu Apr 09 17:45:25 2020 +0200 @@ -747,6 +747,14 @@ }; + enum DicomAssociationRole + { + DicomAssociationRole_Default, + DicomAssociationRole_Scu, + DicomAssociationRole_Scp + }; + + /** * WARNING: Do not change the explicit values in the enumerations * below this point. This would result in incompatible databases diff -r 1237bd0bbdb2 -r f89eac983c9b UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Wed Apr 08 17:00:33 2020 +0200 +++ b/UnitTestsSources/FromDcmtkTests.cpp Thu Apr 09 17:45:25 2020 +0200 @@ -2404,4 +2404,1323 @@ } } + + +#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 +#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 // 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_; + DicomAssociationRole role_; + 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), + role_(DicomAssociationRole_Default) + { + ReadDefaultTimeout(); + } + + DicomAssociationParameters(const std::string& localAet, + const RemoteModalityParameters& remote) : + localAet_(localAet), + remoteAet_(remote.GetApplicationEntityTitle()), + remoteHost_(remote.GetHost()), + remotePort_(remote.GetPortNumber()), + manufacturer_(remote.GetManufacturer()), + role_(DicomAssociationRole_Default), + 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_; + } + + DicomAssociationRole GetRole() const + { + return role_; + } + + 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 SetRole(DicomAssociationRole role) + { + role_ = role; + } + + 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_ && + role_ == other.role_); + } + + 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; + } + } + + void CheckCondition(const OFCondition& cond, + const std::string& command) const + { + 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 \"" + + GetRemoteApplicationEntityTitle() + "\": " + info); + } + } + }; + + + 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 sopClassUid_; + std::set transferSyntaxes_; + }; + + typedef std::map > AcceptedPresentationContexts; + + bool isOpen_; + std::vector proposed_; + AcceptedPresentationContexts accepted_; + T_ASC_Network* net_; + T_ASC_Parameters* params_; + T_ASC_Association* assoc_; + + void Initialize() + { + isOpen_ = false; + net_ = NULL; + params_ = NULL; + assoc_ = NULL; + + // Must be after "isOpen_ = false" + ClearPresentationContexts(); + } + + void CheckConnecting(const DicomAssociationParameters& parameters, + const OFCondition& cond) + { + try + { + parameters.CheckCondition(cond, "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& sopClassUid, + DicomTransferSyntax syntax, + uint8_t presentationContextId) + { + AcceptedPresentationContexts::iterator found = accepted_.find(sopClassUid); + + if (found == accepted_.end()) + { + std::map syntaxes; + syntaxes[syntax] = presentationContextId; + accepted_[sopClassUid] = syntaxes; + } + else + { + if (found->second.find(syntax) != found->second.end()) + { + LOG(WARNING) << "The same transfer syntax (" + << GetTransferSyntaxUid(syntax) + << ") was accepted twice for the same SOP class UID (" + << sopClassUid << ")"; + } + 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 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 (parameters.GetRole()) + { + 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* sopClassUid = proposed_[i].sopClassUid_.c_str(); + + const std::set& source = proposed_[i].transferSyntaxes_; + + std::vector transferSyntaxes; + transferSyntaxes.reserve(source.size()); + + for (std::set::const_iterator + it = source.begin(); it != source.end(); ++it) + { + transferSyntaxes.push_back(GetTransferSyntaxUid(*it)); + } + + assert(!transferSyntaxes.empty()); + CheckConnecting(parameters, ASC_addPresentationContext( + params_, presentationContextId, sopClassUid, + &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& target, + const std::string& sopClassUid) const + { + if (!IsOpen()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened"); + } + + AcceptedPresentationContexts::const_iterator found = accepted_.find(sopClassUid); + + if (found == accepted_.end()) + { + return false; + } + else + { + target = found->second; + return true; + } + } + + void ProposeGenericPresentationContext(const std::string& sopClassUid) + { + std::set ts; + ts.insert(DicomTransferSyntax_LittleEndianImplicit); + ts.insert(DicomTransferSyntax_LittleEndianExplicit); + ts.insert(DicomTransferSyntax_BigEndianExplicit); + ProposePresentationContext(sopClassUid, ts); + } + + void ProposePresentationContext(const std::string& sopClassUid, + DicomTransferSyntax transferSyntax) + { + std::set ts; + ts.insert(transferSyntax); + ProposePresentationContext(sopClassUid, ts); + } + + void ProposePresentationContext(const std::string& sopClassUid, + const std::set& 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.sopClassUid_ = sopClassUid; + 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 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(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 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 fix(fields.Clone()); + + std::set tags; + fix->GetTags(tags); + + for (std::set::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; + } + + parameters_.CheckCondition(cond, "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 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; + } + + parameters_.CheckCondition(cond, "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() + { + SetupPresentationContexts(); + } + + DicomControlUserConnection(const DicomAssociationParameters& params) : + parameters_(params) + { + SetupPresentationContexts(); + } + + void SetParameters(const DicomAssociationParameters& params) + { + if (!parameters_.IsEqual(params)) + { + association_.Close(); + parameters_ = params; + } + } + + const DicomAssociationParameters& GetParameters() const + { + return parameters_; + } + + bool Echo() + { + association_.Open(parameters_); + + DIC_US status; + parameters_.CheckCondition( + DIMSE_echoUser(&association_.GetDcmtkAssociation(), + association_.GetDcmtkAssociation().nextMsgID++, + /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + /*opt_dimse_timeout*/ parameters_.GetTimeout(), + &status, NULL), + "C-ECHO"); + + return status == STATUS_Success; + } + + + void Find(DicomFindAnswers& result, + ResourceType level, + const DicomMap& originalFields, + bool normalize) + { + std::unique_ptr 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); + } + }; + +} + + +TEST(Toto, DISABLED_DicomAssociation) +{ + DicomAssociationParameters params; + params.SetLocalApplicationEntityTitle("ORTHANC"); + params.SetRemoteApplicationEntityTitle("PACS"); + params.SetRemotePort(2001); + +#if 0 + DicomAssociation assoc; + assoc.ProposeGenericPresentationContext(UID_StorageCommitmentPushModelSOPClass); + assoc.ProposeGenericPresentationContext(UID_VerificationSOPClass); + assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, + DicomTransferSyntax_JPEGProcess1); + assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, + DicomTransferSyntax_JPEGProcess2_4); + assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, + DicomTransferSyntax_JPEG2000); + + assoc.Open(params); + + int presID = ASC_findAcceptedPresentationContextID(&assoc.GetDcmtkAssociation(), UID_ComputedRadiographyImageStorage); + printf(">> %d\n", presID); + + std::map pc; + printf(">> %d\n", assoc.LookupAcceptedPresentationContext(pc, UID_ComputedRadiographyImageStorage)); + + for (std::map::const_iterator + it = pc.begin(); it != pc.end(); ++it) + { + printf("[%s] => %d\n", GetTransferSyntaxUid(it->first), it->second); + } +#else + DicomControlUserConnection assoc(params); + + try + { + printf(">> %d\n", assoc.Echo()); + } + catch (OrthancException&) + { + } + + params.SetRemoteApplicationEntityTitle("PACS"); + params.SetRemotePort(2000); + assoc.SetParameters(params); + printf(">> %d\n", assoc.Echo()); + +#endif +} + + +#endif