Mercurial > hg > orthanc
diff Core/DicomNetworking/DicomUserConnection.cpp @ 2382:7284093111b0
big reorganization to cleanly separate framework vs. server
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 29 Aug 2017 21:17:35 +0200 |
parents | OrthancServer/DicomProtocol/DicomUserConnection.cpp@b8969010b534 |
children | 878b59270859 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Tue Aug 29 21:17:35 2017 +0200 @@ -0,0 +1,1220 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017 Osimis, 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/>. + **/ + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + +Copyright (C) 1994-2011, OFFIS e.V. +All rights reserved. + +This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + + +#include "../PrecompiledHeaders.h" +#include "DicomUserConnection.h" + +#include "../DicomFormat/DicomArray.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "../DicomParsing/FromDcmtkBridge.h" +#include "../DicomParsing/ToDcmtkBridge.h" + +#include <dcmtk/dcmdata/dcistrmb.h> +#include <dcmtk/dcmdata/dcistrmf.h> +#include <dcmtk/dcmdata/dcfilefo.h> +#include <dcmtk/dcmdata/dcmetinf.h> +#include <dcmtk/dcmnet/diutil.h> + +#include <set> + + +#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 + + +static const char* DEFAULT_PREFERRED_TRANSFER_SYNTAX = UID_LittleEndianImplicitTransferSyntax; + +/** + * "If we have more than 64 storage SOP classes, tools such as + * storescu will fail because they attempt to negotiate two + * presentation contexts for each SOP class, and there is a total + * limit of 128 contexts for one association." + **/ +static const unsigned int MAXIMUM_STORAGE_SOP_CLASSES = 64; + + +namespace Orthanc +{ + // By default, the timeout for DICOM SCU (client) connections is set to 10 seconds + static uint32_t defaultTimeout_ = 10; + + struct DicomUserConnection::PImpl + { + // Connection state + uint32_t dimseTimeout_; + uint32_t acseTimeout_; + T_ASC_Network* net_; + T_ASC_Parameters* params_; + T_ASC_Association* assoc_; + + bool IsOpen() const + { + return assoc_ != NULL; + } + + void CheckIsOpen() const; + + void Store(DcmInputStream& is, + DicomUserConnection& connection, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID); + }; + + + static void Check(const OFCondition& cond) + { + if (cond.bad()) + { + LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text()); + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + void DicomUserConnection::PImpl::CheckIsOpen() const + { + if (!IsOpen()) + { + LOG(ERROR) << "DicomUserConnection: First open the connection"; + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + + void DicomUserConnection::CheckIsOpen() const + { + pimpl_->CheckIsOpen(); + } + + + static void RegisterStorageSOPClass(T_ASC_Parameters* params, + unsigned int& presentationContextId, + const std::string& sopClass, + const char* asPreferred[], + std::vector<const char*>& asFallback) + { + Check(ASC_addPresentationContext(params, presentationContextId, + sopClass.c_str(), asPreferred, 1)); + presentationContextId += 2; + + if (asFallback.size() > 0) + { + Check(ASC_addPresentationContext(params, presentationContextId, + sopClass.c_str(), &asFallback[0], asFallback.size())); + presentationContextId += 2; + } + } + + + void DicomUserConnection::SetupPresentationContexts(const std::string& preferredTransferSyntax) + { + // Flatten an array with the preferred transfer syntax + const char* asPreferred[1] = { preferredTransferSyntax.c_str() }; + + // Setup the fallback transfer syntaxes + std::set<std::string> fallbackSyntaxes; + fallbackSyntaxes.insert(UID_LittleEndianExplicitTransferSyntax); + fallbackSyntaxes.insert(UID_BigEndianExplicitTransferSyntax); + fallbackSyntaxes.insert(UID_LittleEndianImplicitTransferSyntax); + fallbackSyntaxes.erase(preferredTransferSyntax); + + // Flatten an array with the fallback transfer syntaxes + std::vector<const char*> asFallback; + asFallback.reserve(fallbackSyntaxes.size()); + for (std::set<std::string>::const_iterator + it = fallbackSyntaxes.begin(); it != fallbackSyntaxes.end(); ++it) + { + asFallback.push_back(it->c_str()); + } + + CheckStorageSOPClassesInvariant(); + unsigned int presentationContextId = 1; + + for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin(); + it != reservedStorageSOPClasses_.end(); ++it) + { + RegisterStorageSOPClass(pimpl_->params_, presentationContextId, + *it, asPreferred, asFallback); + } + + for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin(); + it != storageSOPClasses_.end(); ++it) + { + RegisterStorageSOPClass(pimpl_->params_, presentationContextId, + *it, asPreferred, asFallback); + } + + for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin(); + it != defaultStorageSOPClasses_.end(); ++it) + { + RegisterStorageSOPClass(pimpl_->params_, presentationContextId, + *it, asPreferred, asFallback); + } + } + + + static bool IsGenericTransferSyntax(const std::string& syntax) + { + return (syntax == UID_LittleEndianExplicitTransferSyntax || + syntax == UID_BigEndianExplicitTransferSyntax || + syntax == UID_LittleEndianImplicitTransferSyntax); + } + + + void DicomUserConnection::PImpl::Store(DcmInputStream& is, + DicomUserConnection& connection, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + CheckIsOpen(); + + DcmFileFormat dcmff; + Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength)); + + // Determine the storage SOP class UID for this instance + static const DcmTagKey DCM_SOP_CLASS_UID(0x0008, 0x0016); + OFString sopClassUid; + if (dcmff.getDataset()->findAndGetOFString(DCM_SOP_CLASS_UID, sopClassUid).good()) + { + connection.AddStorageSOPClass(sopClassUid.c_str()); + } + + // Determine whether a new presentation context must be + // negotiated, depending on the transfer syntax of this instance + DcmXfer xfer(dcmff.getDataset()->getOriginalXfer()); + const std::string syntax(xfer.getXferID()); + bool isGeneric = IsGenericTransferSyntax(syntax); + + bool renegotiate; + if (isGeneric) + { + // Are we making a generic-to-specific or specific-to-generic change of + // the transfer syntax? If this is the case, renegotiate the connection. + renegotiate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax()); + } + else + { + // We are using a specific transfer syntax. Renegotiate if the + // current connection does not match this transfer syntax. + renegotiate = (syntax != connection.GetPreferredTransferSyntax()); + } + + if (renegotiate) + { + LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated"; + + if (isGeneric) + { + connection.ResetPreferredTransferSyntax(); + } + else + { + connection.SetPreferredTransferSyntax(syntax); + } + } + + if (!connection.IsOpen()) + { + LOG(INFO) << "Renegotiating a C-Store association due to a change in the parameters"; + connection.Open(); + } + + // Figure out which SOP class and SOP instance is encapsulated in the file + DIC_UI sopClass; + DIC_UI sopInstance; + if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sopInstance)) + { + throw OrthancException(ErrorCode_NoSopClassOrInstance); + } + + // Figure out which of the accepted presentation contexts should be used + int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass); + if (presID == 0) + { + const char *modalityName = dcmSOPClassUIDToModality(sopClass); + if (!modalityName) modalityName = dcmFindNameOfUID(sopClass); + if (!modalityName) modalityName = "unknown SOP class"; + throw OrthancException(ErrorCode_NoPresentationContext); + } + + // Prepare the transmission of data + T_DIMSE_C_StoreRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = assoc_->nextMsgID++; + strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); + request.Priority = DIMSE_PRIORITY_MEDIUM; + request.DataSetType = DIMSE_DATASET_PRESENT; + strncpy(request.AffectedSOPInstanceUID, sopInstance, DIC_UI_LEN); + + if (!moveOriginatorAET.empty()) + { + strncpy(request.MoveOriginatorApplicationEntityTitle, + moveOriginatorAET.c_str(), DIC_AE_LEN); + request.opts = O_STORE_MOVEORIGINATORAETITLE; + + request.MoveOriginatorID = moveOriginatorID; // The type DIC_US is an alias for uint16_t + request.opts |= O_STORE_MOVEORIGINATORID; + } + + // Finally conduct transmission of data + T_DIMSE_C_StoreRSP rsp; + DcmDataset* statusDetail = NULL; + Check(DIMSE_storeUser(assoc_, presID, &request, + NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL, + /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_, + &rsp, &statusDetail, NULL)); + + if (statusDetail != NULL) + { + delete statusDetail; + } + } + + + 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 FixFindQuery(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::auto_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); + } + + default: + return new ParsedDicomFile(fields); + } + } + + + static void ExecuteFind(DicomFindAnswers& answers, + T_ASC_Association* association, + DcmDataset* dataset, + const char* sopClass, + bool isWorklist, + const char* level, + uint32_t dimseTimeout) + { + assert(isWorklist ^ (level != NULL)); + + 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, sopClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_DicomFindUnavailable); + } + + T_DIMSE_C_FindRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = association->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; + OFCondition cond = DIMSE_findUser(association, presID, &request, dataset, + FindCallback, &payload, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ dimseTimeout, + &response, &statusDetail); + + if (statusDetail) + { + delete statusDetail; + } + + Check(cond); + } + + + void DicomUserConnection::Find(DicomFindAnswers& result, + ResourceType level, + const DicomMap& originalFields) + { + DicomMap fields; + FixFindQuery(fields, level, originalFields); + + CheckIsOpen(); + + std::auto_ptr<ParsedDicomFile> query(ConvertQueryFields(fields, manufacturer_)); + DcmDataset* dataset = query->GetDcmtkObject().getDataset(); + + const char* clevel = NULL; + const char* sopClass = NULL; + + switch (level) + { + case ResourceType_Patient: + clevel = "PATIENT"; + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "PATIENT"); + sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; + break; + + case ResourceType_Study: + clevel = "STUDY"; + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "STUDY"); + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + + case ResourceType_Series: + clevel = "SERIES"; + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "SERIES"); + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + + case ResourceType_Instance: + clevel = "INSTANCE"; + if (manufacturer_ == ModalityManufacturer_ClearCanvas || + manufacturer_ == ModalityManufacturer_Dcm4Chee) + { + // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>. + // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J + // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "IMAGE"); + } + else + { + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "INSTANCE"); + } + + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // Add the expected tags for this query level. + // WARNING: Do not reorder or add "break" in this switch-case! + switch (level) + { + case ResourceType_Instance: + // SOP Instance UID + if (!fields.HasTag(0x0008, 0x0018)) + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0018), ""); + + case ResourceType_Series: + // Series instance UID + if (!fields.HasTag(0x0020, 0x000e)) + DU_putStringDOElement(dataset, DcmTagKey(0x0020, 0x000e), ""); + + case ResourceType_Study: + // Accession number + if (!fields.HasTag(0x0008, 0x0050)) + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0050), ""); + + // Study instance UID + if (!fields.HasTag(0x0020, 0x000d)) + DU_putStringDOElement(dataset, DcmTagKey(0x0020, 0x000d), ""); + + case ResourceType_Patient: + // Patient ID + if (!fields.HasTag(0x0010, 0x0020)) + DU_putStringDOElement(dataset, DcmTagKey(0x0010, 0x0020), ""); + + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + assert(clevel != NULL && sopClass != NULL); + ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel, pimpl_->dimseTimeout_); + } + + + void DicomUserConnection::MoveInternal(const std::string& targetAet, + ResourceType level, + const DicomMap& fields) + { + CheckIsOpen(); + + std::auto_ptr<ParsedDicomFile> query(ConvertQueryFields(fields, manufacturer_)); + DcmDataset* dataset = query->GetDcmtkObject().getDataset(); + + const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; + switch (level) + { + case ResourceType_Patient: + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "PATIENT"); + break; + + case ResourceType_Study: + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "STUDY"); + break; + + case ResourceType_Series: + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "SERIES"); + break; + + case ResourceType_Instance: + if (manufacturer_ == ModalityManufacturer_ClearCanvas || + manufacturer_ == ModalityManufacturer_Dcm4Chee) + { + // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>. + // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J + // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "IMAGE"); + } + else + { + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "INSTANCE"); + } + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // Figure out which of the accepted presentation contexts should be used + int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_DicomMoveUnavailable); + } + + T_DIMSE_C_MoveRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = pimpl_->assoc_->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(pimpl_->assoc_, presID, &request, dataset, + NULL, NULL, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, + pimpl_->net_, NULL, NULL, + &response, &statusDetail, &responseIdentifiers); + + if (statusDetail) + { + delete statusDetail; + } + + if (responseIdentifiers) + { + delete responseIdentifiers; + } + + Check(cond); + } + + + void DicomUserConnection::ResetStorageSOPClasses() + { + CheckStorageSOPClassesInvariant(); + + storageSOPClasses_.clear(); + defaultStorageSOPClasses_.clear(); + + // Copy the short list of storage SOP classes from DCMTK, making + // room for the 5 SOP classes reserved for C-ECHO, C-FIND, C-MOVE at (**). + + std::set<std::string> uncommon; + uncommon.insert(UID_BlendingSoftcopyPresentationStateStorage); + uncommon.insert(UID_GrayscaleSoftcopyPresentationStateStorage); + uncommon.insert(UID_ColorSoftcopyPresentationStateStorage); + uncommon.insert(UID_PseudoColorSoftcopyPresentationStateStorage); + uncommon.insert(UID_XAXRFGrayscaleSoftcopyPresentationStateStorage); + + // Add the storage syntaxes for C-STORE + for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs - 1; i++) + { + if (uncommon.find(dcmShortSCUStorageSOPClassUIDs[i]) == uncommon.end()) + { + defaultStorageSOPClasses_.insert(dcmShortSCUStorageSOPClassUIDs[i]); + } + } + + CheckStorageSOPClassesInvariant(); + } + + + DicomUserConnection::DicomUserConnection() : + pimpl_(new PImpl), + preferredTransferSyntax_(DEFAULT_PREFERRED_TRANSFER_SYNTAX), + localAet_("STORESCU"), + remoteAet_("ANY-SCP"), + remoteHost_("127.0.0.1") + { + remotePort_ = 104; + manufacturer_ = ModalityManufacturer_Generic; + + SetTimeout(defaultTimeout_); + pimpl_->net_ = NULL; + pimpl_->params_ = NULL; + pimpl_->assoc_ = NULL; + + // SOP classes for C-ECHO, C-FIND and C-MOVE (**) + reservedStorageSOPClasses_.push_back(UID_VerificationSOPClass); + reservedStorageSOPClasses_.push_back(UID_FINDPatientRootQueryRetrieveInformationModel); + reservedStorageSOPClasses_.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); + reservedStorageSOPClasses_.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); + reservedStorageSOPClasses_.push_back(UID_FINDModalityWorklistInformationModel); + + ResetStorageSOPClasses(); + } + + DicomUserConnection::~DicomUserConnection() + { + Close(); + } + + + void DicomUserConnection::SetRemoteModality(const RemoteModalityParameters& parameters) + { + SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); + SetRemoteHost(parameters.GetHost()); + SetRemotePort(parameters.GetPort()); + SetRemoteManufacturer(parameters.GetManufacturer()); + } + + + void DicomUserConnection::SetLocalApplicationEntityTitle(const std::string& aet) + { + if (localAet_ != aet) + { + Close(); + localAet_ = aet; + } + } + + void DicomUserConnection::SetRemoteApplicationEntityTitle(const std::string& aet) + { + if (remoteAet_ != aet) + { + Close(); + remoteAet_ = aet; + } + } + + void DicomUserConnection::SetRemoteManufacturer(ModalityManufacturer manufacturer) + { + if (manufacturer_ != manufacturer) + { + Close(); + manufacturer_ = manufacturer; + } + } + + void DicomUserConnection::ResetPreferredTransferSyntax() + { + SetPreferredTransferSyntax(DEFAULT_PREFERRED_TRANSFER_SYNTAX); + } + + void DicomUserConnection::SetPreferredTransferSyntax(const std::string& preferredTransferSyntax) + { + if (preferredTransferSyntax_ != preferredTransferSyntax) + { + Close(); + preferredTransferSyntax_ = preferredTransferSyntax; + } + } + + + void DicomUserConnection::SetRemoteHost(const std::string& host) + { + if (remoteHost_ != host) + { + if (host.size() > HOST_NAME_MAX - 10) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + Close(); + remoteHost_ = host; + } + } + + void DicomUserConnection::SetRemotePort(uint16_t port) + { + if (remotePort_ != port) + { + Close(); + remotePort_ = port; + } + } + + void DicomUserConnection::Open() + { + if (IsOpen()) + { + // Don't reopen the connection + return; + } + + LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << GetLocalApplicationEntityTitle() + << "\" to AET \"" << GetRemoteApplicationEntityTitle() << "\" on host " + << GetRemoteHost() << ":" << GetRemotePort() + << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")"; + + Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_)); + Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); + + // Set this application's title and the called application's title in the params + Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.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", remoteHost_.c_str(), remotePort_); + + Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort)); + + // Set various options + Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false)); + + SetupPresentationContexts(preferredTransferSyntax_); + + // Do the association + Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_)); + + if (ASC_countAcceptedPresentationContexts(pimpl_->params_) == 0) + { + throw OrthancException(ErrorCode_NoPresentationContext); + } + } + + void DicomUserConnection::Close() + { + if (pimpl_->assoc_ != NULL) + { + ASC_releaseAssociation(pimpl_->assoc_); + ASC_destroyAssociation(&pimpl_->assoc_); + pimpl_->assoc_ = NULL; + pimpl_->params_ = NULL; + } + else + { + if (pimpl_->params_ != NULL) + { + ASC_destroyAssociationParameters(&pimpl_->params_); + pimpl_->params_ = NULL; + } + } + + if (pimpl_->net_ != NULL) + { + ASC_dropNetwork(&pimpl_->net_); + pimpl_->net_ = NULL; + } + } + + bool DicomUserConnection::IsOpen() const + { + return pimpl_->IsOpen(); + } + + void DicomUserConnection::Store(const char* buffer, + size_t size, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + // Prepare an input stream for the memory buffer + DcmInputBufferStream is; + if (size > 0) + is.setBuffer(buffer, size); + is.setEos(); + + pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); + } + + void DicomUserConnection::Store(const std::string& buffer, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + if (buffer.size() > 0) + Store(reinterpret_cast<const char*>(&buffer[0]), buffer.size(), moveOriginatorAET, moveOriginatorID); + else + Store(NULL, 0, moveOriginatorAET, moveOriginatorID); + } + + void DicomUserConnection::StoreFile(const std::string& path, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + // Prepare an input stream for the file + DcmInputFileStream is(path.c_str()); + pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); + } + + bool DicomUserConnection::Echo() + { + CheckIsOpen(); + DIC_US status; + Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, + &status, NULL)); + return status == STATUS_Success; + } + + + 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)); + } + } + + + void DicomUserConnection::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 DicomUserConnection::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 DicomUserConnection::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 DicomUserConnection::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 DicomUserConnection::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 DicomUserConnection::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 DicomUserConnection::SetTimeout(uint32_t seconds) + { + if (seconds == 0) + { + DisableTimeout(); + } + else + { + dcmConnectionTimeout.set(seconds); + pimpl_->dimseTimeout_ = seconds; + pimpl_->acseTimeout_ = 10; // Timeout used during association negociation + } + } + + + void DicomUserConnection::DisableTimeout() + { + /** + * Global timeout (seconds) for connecting to remote hosts. + * Default value is -1 which selects infinite timeout, i.e. blocking connect(). + */ + dcmConnectionTimeout.set(-1); + pimpl_->dimseTimeout_ = 0; + pimpl_->acseTimeout_ = 10; // Timeout used during association negociation + } + + + void DicomUserConnection::CheckStorageSOPClassesInvariant() const + { + assert(storageSOPClasses_.size() + + defaultStorageSOPClasses_.size() + + reservedStorageSOPClasses_.size() <= MAXIMUM_STORAGE_SOP_CLASSES); + } + + void DicomUserConnection::AddStorageSOPClass(const char* sop) + { + CheckStorageSOPClassesInvariant(); + + if (storageSOPClasses_.find(sop) != storageSOPClasses_.end()) + { + // This storage SOP class is already explicitly registered. Do + // nothing. + return; + } + + if (defaultStorageSOPClasses_.find(sop) != defaultStorageSOPClasses_.end()) + { + // This storage SOP class is not explicitly registered, but is + // used by default. Just register it explicitly. + defaultStorageSOPClasses_.erase(sop); + storageSOPClasses_.insert(sop); + + CheckStorageSOPClassesInvariant(); + return; + } + + // This storage SOP class is neither explicitly, nor implicitly + // registered. Close the connection and register it explicitly. + + Close(); + + if (reservedStorageSOPClasses_.size() + + storageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) // (*) + { + // The maximum number of SOP classes is reached + ResetStorageSOPClasses(); + defaultStorageSOPClasses_.erase(sop); + } + else if (reservedStorageSOPClasses_.size() + storageSOPClasses_.size() + + defaultStorageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) + { + // Make room in the default storage syntaxes + assert(!defaultStorageSOPClasses_.empty()); // Necessarily true because condition (*) is false + defaultStorageSOPClasses_.erase(*defaultStorageSOPClasses_.rbegin()); + } + + // Explicitly register the new storage syntax + storageSOPClasses_.insert(sop); + + CheckStorageSOPClassesInvariant(); + } + + + void DicomUserConnection::FindWorklist(DicomFindAnswers& result, + ParsedDicomFile& query) + { + CheckIsOpen(); + + DcmDataset* dataset = query.GetDcmtkObject().getDataset(); + const char* sopClass = UID_FINDModalityWorklistInformationModel; + + ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true, NULL, pimpl_->dimseTimeout_); + } + + + void DicomUserConnection::SetDefaultTimeout(uint32_t seconds) + { + LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " + << seconds << " seconds (0 = no timeout)"; + defaultTimeout_ = seconds; + } +}