Mercurial > hg > orthanc
diff OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp @ 4044:d25f4c0fa160 framework
splitting code into OrthancFramework and OrthancServer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 10 Jun 2020 20:30:34 +0200 |
parents | Core/DicomNetworking/DicomControlUserConnection.cpp@cc6ed76bba27 |
children | bf7b9edf6b81 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Wed Jun 10 20:30:34 2020 +0200 @@ -0,0 +1,674 @@ +/** + * 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 "../Compatibility.h" +#include "../DicomFormat/DicomArray.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() + { + assert(association_.get() != NULL); + 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)); + assert(association_.get() != 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_.GetRemoteModality().GetApplicationEntityTitle()); + } + + 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_.GetRemoteModality().GetApplicationEntityTitle() + + "\" has failed with DIMSE status 0x" + buf + + " (unable to process - invalid query ?)"); + } + else + { + throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + + "\" has failed with DIMSE status 0x" + buf); + } + } + } + + + void DicomControlUserConnection::MoveInternal(const std::string& targetAet, + ResourceType level, + const DicomMap& fields) + { + assert(association_.get() != NULL); + association_->Open(parameters_); + + std::unique_ptr<ParsedDicomFile> query( + ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer())); + 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_.GetRemoteModality().GetApplicationEntityTitle()); + } + + 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_.GetRemoteModality().GetApplicationEntityTitle() + + "\" has failed with DIMSE status 0x" + buf + + " (unable to process - resource not found ?)"); + } + else + { + throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + + "\" has failed with DIMSE status 0x" + buf); + } + } + } + + + DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) : + parameters_(params), + association_(new DicomAssociation) + { + SetupPresentationContexts(); + } + + + void DicomControlUserConnection::Close() + { + assert(association_.get() != NULL); + association_->Close(); + } + + + bool DicomControlUserConnection::Echo() + { + assert(association_.get() != NULL); + 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_.GetRemoteModality().GetManufacturer())); + } + 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_.GetRemoteModality().GetManufacturer() == 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); + } +}