# HG changeset patch # User Sebastien Jodogne # Date 1587479845 -7200 # Node ID 3ab2d48c8f6900f57972b6145005dbdab7a2f544 # Parent 4f78da5613a19ebf93c0258d6f591b8f64b34ddb# Parent dd0fcbf6a791522999877e6a059599aa1e6e5bd8 integration mainline->c-get diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomFormat/DicomMap.cpp --- a/Core/DicomFormat/DicomMap.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/DicomFormat/DicomMap.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -745,7 +745,7 @@ } - bool DicomMap::IsDicomFile(const char* dicom, + bool DicomMap::IsDicomFile(const void* dicom, size_t size) { /** @@ -755,16 +755,18 @@ * account to determine whether the file is or is not a DICOM file. **/ + const uint8_t* p = reinterpret_cast(dicom); + return (size >= 132 && - dicom[128] == 'D' && - dicom[129] == 'I' && - dicom[130] == 'C' && - dicom[131] == 'M'); + p[128] == 'D' && + p[129] == 'I' && + p[130] == 'C' && + p[131] == 'M'); } bool DicomMap::ParseDicomMetaInformation(DicomMap& result, - const char* dicom, + const void* dicom, size_t size) { if (!IsDicomFile(dicom, size)) @@ -788,7 +790,7 @@ DicomTag tag(0x0000, 0x0000); // Dummy initialization ValueRepresentation vr; std::string value; - if (!ReadNextTag(tag, vr, value, dicom, size, position) || + if (!ReadNextTag(tag, vr, value, reinterpret_cast(dicom), size, position) || tag.GetGroup() != 0x0002 || tag.GetElement() != 0x0000 || vr != ValueRepresentation_UnsignedLong || @@ -805,7 +807,7 @@ while (position < stopPosition) { - if (ReadNextTag(tag, vr, value, dicom, size, position)) + if (ReadNextTag(tag, vr, value, reinterpret_cast(dicom), size, position)) { result.SetValue(tag, value, IsBinaryValueRepresentation(vr)); } diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomFormat/DicomMap.h --- a/Core/DicomFormat/DicomMap.h Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/DicomFormat/DicomMap.h Tue Apr 21 16:37:25 2020 +0200 @@ -180,11 +180,11 @@ void GetTags(std::set& tags) const; - static bool IsDicomFile(const char* dicom, + static bool IsDicomFile(const void* dicom, size_t size); static bool ParseDicomMetaInformation(DicomMap& result, - const char* dicom, + const void* dicom, size_t size); void LogMissingTagsForStore() const; diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/DicomAssociation.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomAssociation.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -0,0 +1,859 @@ +/** + * 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 . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomAssociation.h" + +#if !defined(DCMTK_VERSION_NUMBER) +# error The macro DCMTK_VERSION_NUMBER must be defined +#endif + +#include "../Compatibility.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "NetworkingCompatibility.h" + +#include // For dcmConnectionTimeout() +#include + +namespace Orthanc +{ + static void FillSopSequence(DcmDataset& dataset, + const DcmTagKey& tag, + const std::vector& sopClassUids, + const std::vector& sopInstanceUids, + const std::vector& 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 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 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& e) + { + // Don't throw exception in destructors + LOG(ERROR) << "Error while destroying a DICOM association: " << e.What(); + } + } + + + 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& 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, 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& 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 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 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& 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, + "DicomAssociation - " + command + " to AET \"" + + parameters.GetRemoteApplicationEntityTitle() + + "\": " + info); + } + } + + + void DicomAssociation::ReportStorageCommitment( + const DicomAssociationParameters& parameters, + const std::string& transactionUid, + const std::vector& sopClassUids, + const std::vector& sopInstanceUids, + const std::vector& failureReasons) + { + if (sopClassUids.size() != sopInstanceUids.size() || + sopClassUids.size() != failureReasons.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + + std::vector successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids; + std::vector 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 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 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& sopClassUids, + const std::vector& 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 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 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(); + } +} diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/DicomAssociation.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomAssociation.h Tue Apr 21 16:37:25 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 . + **/ + + +#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 + +#include // For uint8_t +#include +#include + +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 transferSyntaxes_; + }; + + typedef std::map > + AcceptedPresentationContexts; + + DicomAssociationRole role_; + bool isOpen_; + std::vector 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& 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& 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& sopClassUids, + const std::vector& sopInstanceUids, + const std::vector& failureReasons); + + static void RequestStorageCommitment( + const DicomAssociationParameters& parameters, + const std::string& transactionUid, + const std::vector& sopClassUids, + const std::vector& sopInstanceUids); + }; +} diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/DicomAssociationParameters.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomAssociationParameters.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -0,0 +1,123 @@ +/** + * 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 . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomAssociationParameters.h" + +#include "../Compatibility.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "NetworkingCompatibility.h" + +#include + +// 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; + } + } +} diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/DicomAssociationParameters.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomAssociationParameters.h Tue Apr 21 16:37:25 2020 +0200 @@ -0,0 +1,128 @@ +/** + * 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 . + **/ + + +#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); + }; +} diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/DicomControlUserConnection.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomControlUserConnection.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -0,0 +1,681 @@ +/** + * 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 . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomControlUserConnection.h" + +#include "../Compatibility.h" +#include "../DicomParsing/FromDcmtkBridge.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "DicomAssociation.h" + +#include +#include + +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(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 */); + } + } + + + + 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_.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) + { + assert(association_.get() != NULL); + 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; + } + + 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(); + } + + + DicomControlUserConnection::DicomControlUserConnection(const std::string& localAet, + const RemoteModalityParameters& remote) : + parameters_(localAet, remote), + 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 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); + } +} diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/DicomControlUserConnection.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomControlUserConnection.h Tue Apr 21 16:37:25 2020 +0200 @@ -0,0 +1,112 @@ +/** + * 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 . + **/ + + +#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 + +namespace Orthanc +{ + class DicomAssociation; // Forward declaration for PImpl design pattern + + class DicomControlUserConnection : public boost::noncopyable + { + private: + DicomAssociationParameters parameters_; + boost::shared_ptr 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 std::string& localAet, + const RemoteModalityParameters& remote); + + DicomControlUserConnection(const DicomAssociationParameters& params); + + const DicomAssociationParameters& GetParameters() const + { + return parameters_; + } + + void Close(); + + 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); + }; +} diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/DicomStoreUserConnection.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomStoreUserConnection.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -0,0 +1,359 @@ +/** + * 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 . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomStoreUserConnection.h" + +#include "../DicomParsing/FromDcmtkBridge.h" +#include "../DicomParsing/ParsedDicomFile.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "DicomAssociation.h" + +#include + + +namespace Orthanc +{ + bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid, + const std::set& syntaxes) + { + size_t requiredCount = syntaxes.size(); + if (proposeUncompressedSyntaxes_) + { + requiredCount += 1; + } + + if (association_->GetRemainingPropositions() <= requiredCount) + { + return false; // Not enough room + } + + for (std::set::const_iterator + it = syntaxes.begin(); it != syntaxes.end(); ++it) + { + association_->ProposePresentationContext(sopClassUid, *it); + } + + if (proposeUncompressedSyntaxes_) + { + std::set 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 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 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 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); + } + + + void DicomStoreUserConnection::Store(std::string& sopClassUid, + std::string& sopInstanceUid, + DcmDataset& dataset, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + OFString a, b; + if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() || + !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good()) + { + throw OrthancException(ErrorCode_NoSopClassOrInstance, + "Unable to determine the SOP class/instance for C-STORE with AET " + + parameters_.GetRemoteApplicationEntityTitle()); + } + + sopClassUid.assign(a.c_str()); + sopInstanceUid.assign(b.c_str()); + + DicomTransferSyntax transferSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax( + transferSyntax, dataset.getOriginalXfer())) + { + throw OrthancException(ErrorCode_InternalError, + "Unknown transfer syntax from DCMTK"); + } + + // Figure out which accepted presentation context should be used + uint8_t presID; + if (!NegotiatePresentationContext(presID, sopClassUid.c_str(), transferSyntax)) + { + throw OrthancException(ErrorCode_InternalError, + "No valid presentation context was negotiated upfront"); + } + + // Prepare the transmission of data + T_DIMSE_C_StoreRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = association_->GetDcmtkAssociation().nextMsgID++; + strncpy(request.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN); + request.Priority = DIMSE_PRIORITY_MEDIUM; + request.DataSetType = DIMSE_DATASET_PRESENT; + strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), 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 response; + DcmDataset* statusDetail = NULL; + DicomAssociation::CheckCondition( + DIMSE_storeUser(&association_->GetDcmtkAssociation(), presID, &request, + NULL, &dataset, /*progressCallback*/ NULL, NULL, + /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + /*opt_dimse_timeout*/ GetParameters().GetTimeout(), + &response, &statusDetail, NULL), + GetParameters(), "C-STORE"); + + if (statusDetail != NULL) + { + delete statusDetail; + } + + /** + * New in Orthanc 1.6.0: Deal with failures during C-STORE. + * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1 + **/ + + if (response.DimseStatus != 0x0000 && // Success + response.DimseStatus != 0xB000 && // Warning - Coercion of Data Elements + response.DimseStatus != 0xB007 && // Warning - Data Set does not match SOP Class + response.DimseStatus != 0xB006) // Warning - Elements Discarded + { + char buf[16]; + sprintf(buf, "%04X", response.DimseStatus); + throw OrthancException(ErrorCode_NetworkProtocol, + "C-STORE SCU to AET \"" + + GetParameters().GetRemoteApplicationEntityTitle() + + "\" has failed with DIMSE status 0x" + buf); + } + } + + + void DicomStoreUserConnection::Store(std::string& sopClassUid, + std::string& sopInstanceUid, + ParsedDicomFile& parsed, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + Store(sopClassUid, sopInstanceUid, *parsed.GetDcmtkObject().getDataset(), + moveOriginatorAET, moveOriginatorID); + } + + + void DicomStoreUserConnection::Store(std::string& sopClassUid, + std::string& sopInstanceUid, + const void* buffer, + size_t size, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + std::unique_ptr dicom( + FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size)); + + Store(sopClassUid, sopInstanceUid, *dicom->getDataset(), + moveOriginatorAET, moveOriginatorID); + } +} diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/DicomStoreUserConnection.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomStoreUserConnection.h Tue Apr 21 16:37:25 2020 +0200 @@ -0,0 +1,155 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "DicomAssociationParameters.h" + +#include +#include +#include +#include // For uint8_t + + +class DcmDataset; + +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 ParsedDicomFile; + + class DicomStoreUserConnection : public boost::noncopyable + { + private: + typedef std::map > StorageClasses; + + DicomAssociationParameters parameters_; + boost::shared_ptr 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& syntaxes); + + // Should only be used if transcoding + bool LookupPresentationContext(uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax); + + public: + DicomStoreUserConnection(const DicomAssociationParameters& params); + + const DicomAssociationParameters& GetParameters() const + { + return parameters_; + } + + void SetCommonClassesProposed(bool proposed) + { + proposeCommonClasses_ = proposed; + } + + bool IsCommonClassesProposed() const + { + return proposeCommonClasses_; + } + + void SetUncompressedSyntaxesProposed(bool proposed) + { + proposeUncompressedSyntaxes_ = proposed; + } + + bool IsUncompressedSyntaxesProposed() const + { + return proposeUncompressedSyntaxes_; + } + + void SetRetiredBigEndianProposed(bool propose) + { + proposeRetiredBigEndian_ = propose; + } + + bool IsRetiredBigEndianProposed() const + { + return proposeRetiredBigEndian_; + } + + void PrepareStorageClass(const std::string& sopClassUid, + DicomTransferSyntax syntax); + + // TODO => to private + bool NegotiatePresentationContext(uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax); + + void Store(std::string& sopClassUid, + std::string& sopInstanceUid, + DcmDataset& dataset, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID); + + void Store(std::string& sopClassUid, + std::string& sopInstanceUid, + ParsedDicomFile& parsed, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID); + + void Store(std::string& sopClassUid, + std::string& sopInstanceUid, + const void* buffer, + size_t size, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID); + }; +} diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/DicomUserConnection.cpp --- a/Core/DicomNetworking/DicomUserConnection.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -92,6 +92,7 @@ #include "../OrthancException.h" #include "../DicomParsing/FromDcmtkBridge.h" #include "../DicomParsing/ToDcmtkBridge.h" +#include "NetworkingCompatibility.h" #include #include @@ -103,30 +104,6 @@ #include -#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 - - static const char* DEFAULT_PREFERRED_TRANSFER_SYNTAX = UID_LittleEndianImplicitTransferSyntax; /** diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/IDicomConnectionManager.h --- a/Core/DicomNetworking/IDicomConnectionManager.h Fri Mar 27 10:06:58 2020 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,82 +0,0 @@ -/** - * 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 . - **/ - - -#pragma once - -#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING) -# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined -#endif - -#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0 - -namespace Orthanc -{ - // DICOM networking is disabled, this is just a void class - class IDicomConnectionManager : public boost::noncopyable - { - public: - virtual ~IDicomConnectionManager() - { - } - }; -} - -#else - -#include "DicomUserConnection.h" - -namespace Orthanc -{ - class IDicomConnectionManager : public boost::noncopyable - { - public: - virtual ~IDicomConnectionManager() - { - } - - class IResource : public boost::noncopyable - { - public: - virtual ~IResource() - { - } - - virtual DicomUserConnection& GetConnection() = 0; - }; - - virtual IResource* AcquireConnection(const std::string& localAet, - const RemoteModalityParameters& remote) = 0; - }; -} - -#endif diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/NetworkingCompatibility.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/NetworkingCompatibility.h Tue Apr 21 16:37:25 2020 +0200 @@ -0,0 +1,58 @@ +/** + * 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 . + **/ + + +#pragma once + + +#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 diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/TimeoutDicomConnectionManager.cpp --- a/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -44,64 +44,59 @@ return boost::posix_time::microsec_clock::universal_time(); } - class TimeoutDicomConnectionManager::Resource : public IDicomConnectionManager::IResource + + TimeoutDicomConnectionManager::Lock::Lock(TimeoutDicomConnectionManager& that, + const std::string& localAet, + const RemoteModalityParameters& remote) : + that_(that), + lock_(that_.mutex_) { - private: - TimeoutDicomConnectionManager& that_; + // Calling "Touch()" will be done by the "~Lock()" destructor + that_.OpenInternal(localAet, remote); + } - public: - Resource(TimeoutDicomConnectionManager& that) : - that_(that) + + TimeoutDicomConnectionManager::Lock::~Lock() + { + that_.TouchInternal(); + } + + + DicomUserConnection& TimeoutDicomConnectionManager::Lock::GetConnection() + { + if (that_.connection_.get() == NULL) { - if (that_.connection_.get() == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } + // The allocation should have been done by "that_.Open()" in the constructor + throw OrthancException(ErrorCode_InternalError); } - - ~Resource() + else { - that_.Touch(); - } - - DicomUserConnection& GetConnection() - { - assert(that_.connection_.get() != NULL); return *that_.connection_; } - }; + } - void TimeoutDicomConnectionManager::Touch() + // Mutex must be locked + void TimeoutDicomConnectionManager::TouchInternal() { lastUse_ = GetNow(); } - void TimeoutDicomConnectionManager::CheckTimeoutInternal() + // Mutex must be locked + void TimeoutDicomConnectionManager::OpenInternal(const std::string& localAet, + const RemoteModalityParameters& remote) { - if (connection_.get() != NULL && - (GetNow() - lastUse_) >= timeout_) + if (connection_.get() == NULL || + !connection_->IsSameAssociation(localAet, remote)) { - Close(); + connection_.reset(new DicomUserConnection(localAet, remote)); } } - void TimeoutDicomConnectionManager::SetTimeout(unsigned int timeout) - { - timeout_ = boost::posix_time::milliseconds(timeout); - CheckTimeoutInternal(); - } - - - unsigned int TimeoutDicomConnectionManager::GetTimeout() - { - return static_cast(timeout_.total_milliseconds()); - } - - - void TimeoutDicomConnectionManager::Close() + // Mutex must be locked + void TimeoutDicomConnectionManager::CloseInternal() { if (connection_.get() != NULL) { @@ -113,22 +108,29 @@ } - void TimeoutDicomConnectionManager::CheckTimeout() + void TimeoutDicomConnectionManager::SetInactivityTimeout(unsigned int milliseconds) { - CheckTimeoutInternal(); + boost::mutex::scoped_lock lock(mutex_); + timeout_ = boost::posix_time::milliseconds(milliseconds); + CloseInternal(); } - IDicomConnectionManager::IResource* - TimeoutDicomConnectionManager::AcquireConnection(const std::string& localAet, - const RemoteModalityParameters& remote) + unsigned int TimeoutDicomConnectionManager::GetInactivityTimeout() { - if (connection_.get() == NULL || - !connection_->IsSameAssociation(localAet, remote)) + boost::mutex::scoped_lock lock(mutex_); + return static_cast(timeout_.total_milliseconds()); + } + + + void TimeoutDicomConnectionManager::CloseIfInactive() + { + boost::mutex::scoped_lock lock(mutex_); + + if (connection_.get() != NULL && + (GetNow() - lastUse_) >= timeout_) { - connection_.reset(new DicomUserConnection(localAet, remote)); + CloseInternal(); } - - return new Resource(*this); } } diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomNetworking/TimeoutDicomConnectionManager.h --- a/Core/DicomNetworking/TimeoutDicomConnectionManager.h Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.h Tue Apr 21 16:37:25 2020 +0200 @@ -33,72 +33,72 @@ #pragma once -#include "IDicomConnectionManager.h" +#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING) +# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined +#endif -#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0 +#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1 +# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be 1 to use this file +#endif + + +#include "../Compatibility.h" +#include "DicomUserConnection.h" + +#include +#include namespace Orthanc { - class TimeoutDicomConnectionManager : public IDicomConnectionManager - { - public: - void SetTimeout(unsigned int timeout) - { - } - - unsigned int GetTimeout() - { - return 0; - } - - void Close() - { - } - - void CheckTimeout() - { - } - }; -} - -#else - -#include "../Compatibility.h" - -#include - -namespace Orthanc -{ - class TimeoutDicomConnectionManager : public IDicomConnectionManager + /** + * This class corresponds to a singleton to a DICOM SCU connection. + **/ + class TimeoutDicomConnectionManager : public boost::noncopyable { private: - class Resource; - + boost::mutex mutex_; std::unique_ptr connection_; boost::posix_time::ptime lastUse_; boost::posix_time::time_duration timeout_; - void Touch(); + // Mutex must be locked + void TouchInternal(); - void CheckTimeoutInternal(); + // Mutex must be locked + void OpenInternal(const std::string& localAet, + const RemoteModalityParameters& remote); + + // Mutex must be locked + void CloseInternal(); public: + class Lock : public boost::noncopyable + { + private: + TimeoutDicomConnectionManager& that_; + boost::mutex::scoped_lock lock_; + + public: + Lock(TimeoutDicomConnectionManager& that, + const std::string& localAet, + const RemoteModalityParameters& remote); + + ~Lock(); + + DicomUserConnection& GetConnection(); + }; + TimeoutDicomConnectionManager() : timeout_(boost::posix_time::milliseconds(1000)) { } - void SetTimeout(unsigned int timeout); + void SetInactivityTimeout(unsigned int milliseconds); - unsigned int GetTimeout(); + unsigned int GetInactivityTimeout(); // In milliseconds void Close(); - void CheckTimeout(); - - virtual IResource* AcquireConnection(const std::string& localAet, - const RemoteModalityParameters& remote); + void CloseIfInactive(); }; } - -#endif diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/DicomParsing/ParsedDicomFile.cpp --- a/Core/DicomParsing/ParsedDicomFile.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -952,7 +952,7 @@ * equals the empty string, then proceed. In Orthanc <= 1.5.6, * an exception "Bad file format" was generated. * https://groups.google.com/d/msg/orthanc-users/aphG_h1AHVg/rfOTtTPTAgAJ - * https://bitbucket.org/sjodogne/orthanc/commits/4c45e018bd3de3cfa21d6efc6734673aaaee4435 + * https://hg.orthanc-server.com/orthanc/rev/4c45e018bd3de3cfa21d6efc6734673aaaee4435 **/ patientId.clear(); } diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/Enumerations.h --- a/Core/Enumerations.h Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/Enumerations.h Tue Apr 21 16:37:25 2020 +0200 @@ -748,6 +748,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 4f78da5613a1 -r 3ab2d48c8f69 Core/HttpServer/HttpServer.cpp --- a/Core/HttpServer/HttpServer.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/HttpServer/HttpServer.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -170,7 +170,7 @@ - class ChunkStore + class ChunkStore : public boost::noncopyable { private: typedef std::list Content; @@ -308,7 +308,7 @@ struct mg_connection *connection, const std::string& contentLength) { - int length; + int length; try { length = boost::lexical_cast(contentLength); @@ -904,7 +904,6 @@ } } - if (!found && server.HasHandler()) { diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/Images/PamReader.cpp --- a/Core/Images/PamReader.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/Images/PamReader.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -42,6 +42,7 @@ # include "../SystemToolbox.h" #endif +#include // For malloc/free #include #include @@ -200,7 +201,30 @@ } size_t offset = content_.size() - pitch * height; - AssignWritable(format, width, height, pitch, &content_[offset]); + + { + intptr_t bufferAddr = reinterpret_cast(&content_[offset]); + if((bufferAddr % 8) == 0) + LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr; + else + LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr << " (not a multiple of 8!)"; + } + + // if we want to enforce alignment, we need to use a freshly allocated + // buffer, since we have no alignment guarantees on the original one + if (enforceAligned_) + { + if (alignedImageBuffer_ != NULL) + free(alignedImageBuffer_); + alignedImageBuffer_ = malloc(pitch * height); + memcpy(alignedImageBuffer_, &content_[offset], pitch* height); + content_ = ""; + AssignWritable(format, width, height, pitch, alignedImageBuffer_); + } + else + { + AssignWritable(format, width, height, pitch, &content_[offset]); + } // Byte swapping if needed if (bytesPerChannel != 1 && @@ -231,7 +255,7 @@ at SAFE_HEAP_LOAD_i32_2_2 (wasm-function[251132]:39) at __ZN7Orthanc9PamReader12ParseContentEv (wasm-function[11457]:8088) - Web Assenmbly IS LITTLE ENDIAN! + Web Assembly IS LITTLE ENDIAN! Perhaps in htobe16 ? */ @@ -274,4 +298,12 @@ content_.assign(reinterpret_cast(buffer), size); ParseContent(); } + + PamReader::~PamReader() + { + if (alignedImageBuffer_ != NULL) + { + free(alignedImageBuffer_); + } + } } diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/Images/PamReader.h --- a/Core/Images/PamReader.h Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/Images/PamReader.h Tue Apr 21 16:37:25 2020 +0200 @@ -46,9 +46,41 @@ private: void ParseContent(); + /** + Whether we want to use the default malloc alignment in the image buffer, + at the expense of an extra copy + */ + bool enforceAligned_; + + /** + This is actually a copy of wrappedContent_, but properly aligned. + + It is only used if the enforceAligned parameter is set to true in the + constructor. + */ + void* alignedImageBuffer_; + + /** + Points somewhere in the content_ buffer. + */ + ImageAccessor wrappedContent_; + + /** + Raw content (file bytes or answer from the server, for instance). + */ std::string content_; public: + /** + See doc for field enforceAligned_ + */ + PamReader(bool enforceAligned = false) : + enforceAligned_(enforceAligned), + alignedImageBuffer_(NULL) + { + } + + virtual ~PamReader(); #if ORTHANC_SANDBOXED == 0 void ReadFromFile(const std::string& filename); diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/JobsEngine/Operations/IJobOperation.h --- a/Core/JobsEngine/Operations/IJobOperation.h Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/JobsEngine/Operations/IJobOperation.h Tue Apr 21 16:37:25 2020 +0200 @@ -34,7 +34,6 @@ #pragma once #include "JobOperationValues.h" -#include "../../DicomNetworking/IDicomConnectionManager.h" namespace Orthanc { @@ -46,8 +45,7 @@ } virtual void Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& dicomConnection) = 0; + const JobOperationValue& input) = 0; virtual void Serialize(Json::Value& result) const = 0; }; diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/JobsEngine/Operations/LogJobOperation.cpp --- a/Core/JobsEngine/Operations/LogJobOperation.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/JobsEngine/Operations/LogJobOperation.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -40,8 +40,7 @@ namespace Orthanc { void LogJobOperation::Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager) + const JobOperationValue& input) { switch (input.GetType()) { diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/JobsEngine/Operations/LogJobOperation.h --- a/Core/JobsEngine/Operations/LogJobOperation.h Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/JobsEngine/Operations/LogJobOperation.h Tue Apr 21 16:37:25 2020 +0200 @@ -41,8 +41,7 @@ { public: virtual void Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager); + const JobOperationValue& input); virtual void Serialize(Json::Value& result) const { diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp --- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -43,7 +43,6 @@ { static const char* CURRENT = "Current"; static const char* DESCRIPTION = "Description"; - static const char* DICOM_TIMEOUT = "DicomTimeout"; static const char* NEXT_OPERATIONS = "Next"; static const char* OPERATION = "Operation"; static const char* OPERATIONS = "Operations"; @@ -127,7 +126,7 @@ return currentInput_ >= originalInputs_->GetSize() + workInputs_->GetSize(); } - void Step(IDicomConnectionManager& connectionManager) + void Step() { if (IsDone()) { @@ -146,7 +145,7 @@ } JobOperationValues outputs; - operation_->Apply(outputs, *input, connectionManager); + operation_->Apply(outputs, *input); if (!nextOperations_.empty()) { @@ -254,12 +253,6 @@ } - void SequenceOfOperationsJob::Lock::SetDicomAssociationTimeout(unsigned int timeout) - { - that_.connectionManager_.SetTimeout(timeout); - } - - size_t SequenceOfOperationsJob::Lock::AddOperation(IJobOperation* operation) { if (IsDone()) @@ -341,7 +334,6 @@ (*it)->SignalDone(*this); } - connectionManager_.Close(); return JobStepResult::Success(); } else @@ -360,11 +352,9 @@ if (current_ < operations_.size()) { - operations_[current_]->Step(connectionManager_); + operations_[current_]->Step(); } - connectionManager_.CheckTimeout(); - return JobStepResult::Continue(); } @@ -383,13 +373,6 @@ } - void SequenceOfOperationsJob::Stop(JobStopReason reason) - { - boost::mutex::scoped_lock lock(mutex_); - connectionManager_.Close(); - } - - float SequenceOfOperationsJob::GetProgress() { boost::mutex::scoped_lock lock(mutex_); @@ -420,7 +403,6 @@ value[DESCRIPTION] = description_; value[TRAILING_TIMEOUT] = static_cast(trailingTimeout_.total_milliseconds()); - value[DICOM_TIMEOUT] = connectionManager_.GetTimeout(); value[CURRENT] = static_cast(current_); Json::Value tmp = Json::arrayValue; @@ -454,8 +436,6 @@ description_ = SerializationToolbox::ReadString(serialized, DESCRIPTION); trailingTimeout_ = boost::posix_time::milliseconds (SerializationToolbox::ReadUnsignedInteger(serialized, TRAILING_TIMEOUT)); - connectionManager_.SetTimeout - (SerializationToolbox::ReadUnsignedInteger(serialized, DICOM_TIMEOUT)); current_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT); const Json::Value& ops = serialized[OPERATIONS]; diff -r 4f78da5613a1 -r 3ab2d48c8f69 Core/JobsEngine/Operations/SequenceOfOperationsJob.h --- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Fri Mar 27 10:06:58 2020 -0400 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Tue Apr 21 16:37:25 2020 +0200 @@ -36,8 +36,6 @@ #include "../IJob.h" #include "IJobOperation.h" -#include "../../DicomNetworking/TimeoutDicomConnectionManager.h" - #include #include @@ -69,7 +67,6 @@ boost::condition_variable operationAdded_; boost::posix_time::time_duration trailingTimeout_; std::list observers_; - TimeoutDicomConnectionManager connectionManager_; void NotifyDone() const; @@ -109,8 +106,6 @@ } void SetTrailingOperationTimeout(unsigned int timeout); - - void SetDicomAssociationTimeout(unsigned int timeout); size_t AddOperation(IJobOperation* operation); @@ -134,7 +129,9 @@ virtual void Reset() ORTHANC_OVERRIDE; - virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE + { + } virtual float GetProgress() ORTHANC_OVERRIDE; diff -r 4f78da5613a1 -r 3ab2d48c8f69 NEWS --- a/NEWS Fri Mar 27 10:06:58 2020 -0400 +++ b/NEWS Tue Apr 21 16:37:25 2020 +0200 @@ -2,6 +2,9 @@ =============================== +Version 1.6.1 (2020-04-21) +========================== + REST API -------- @@ -10,6 +13,19 @@ - "/modalities/{id}/store-straight": Synchronously send the DICOM instance in POST body to another modality (alternative to command-line tools such as "storescu") +Plugins +------- + +* New functions in the SDK: + - OrthancPluginRegisterIncomingDicomInstanceFilter() + - OrthancPluginGetInstanceTransferSyntaxUid() + - OrthancPluginHasInstancePixelData() + +Lua +--- + +* New "info" field in "ReceivedInstanceFilter()" callback, containing + "HasPixelData" and "TransferSyntaxUID" information Maintenance ----------- @@ -19,6 +35,8 @@ * Fix lookup form in Orthanc Explorer (wildcards not allowed in StudyDate) * Fix signature of "OrthancPluginRegisterStorageCommitmentScpCallback()" in plugins SDK * Error reporting on failure while initializing SSL +* Fix unit test ParsedDicomFile.ToJsonFlags2 on big-endian architectures +* Avoid one memcpy of the DICOM buffer on "POST /instances" * Upgraded dependencies for static builds (notably on Windows): - civetweb 1.12 - openssl 1.1.1f diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/DicomInstanceToStore.cpp --- a/OrthancServer/DicomInstanceToStore.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/DicomInstanceToStore.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -150,18 +150,48 @@ { public: DicomInstanceOrigin origin_; - SmartContainer buffer_; + bool hasBuffer_; + std::unique_ptr ownBuffer_; + const void* bufferData_; + size_t bufferSize_; SmartContainer parsed_; SmartContainer summary_; SmartContainer json_; MetadataMap metadata_; + PImpl() : + hasBuffer_(false), + bufferData_(NULL), + bufferSize_(0) + { + } + private: std::unique_ptr hasher_; + void ParseDicomFile() + { + if (!parsed_.HasContent()) + { + if (!hasBuffer_) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (ownBuffer_.get() != NULL) + { + parsed_.TakeOwnership(new ParsedDicomFile(*ownBuffer_)); + } + else + { + parsed_.TakeOwnership(new ParsedDicomFile(bufferData_, bufferSize_)); + } + } + } + void ComputeMissingInformation() { - if (buffer_.HasContent() && + if (hasBuffer_ && summary_.HasContent() && json_.HasContent()) { @@ -169,7 +199,7 @@ return; } - if (!buffer_.HasContent()) + if (!hasBuffer_) { if (!parsed_.HasContent()) { @@ -186,13 +216,15 @@ } // Serialize the parsed DICOM file - buffer_.Allocate(); - if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), + ownBuffer_.reset(new std::string); + if (!FromDcmtkBridge::SaveToMemoryBuffer(*ownBuffer_, *parsed_.GetContent().GetDcmtkObject().getDataset())) { throw OrthancException(ErrorCode_InternalError, "Unable to serialize a DICOM file to a memory buffer"); } + + hasBuffer_ = true; } if (summary_.HasContent() && @@ -205,10 +237,8 @@ // memory buffer, but that its summary or its JSON version is // missing - if (!parsed_.HasContent()) - { - parsed_.TakeOwnership(new ParsedDicomFile(buffer_.GetConstContent())); - } + ParseDicomFile(); + assert(parsed_.HasContent()); // At this point, we have parsed the DICOM file @@ -232,22 +262,38 @@ public: - const char* GetBufferData() + void SetBuffer(const void* data, + size_t size) + { + ownBuffer_.reset(NULL); + bufferData_ = data; + bufferSize_ = size; + hasBuffer_ = true; + } + + const void* GetBufferData() { ComputeMissingInformation(); - - if (!buffer_.HasContent()) + + if (!hasBuffer_) { throw OrthancException(ErrorCode_InternalError); } - if (buffer_.GetConstContent().size() == 0) + if (ownBuffer_.get() != NULL) { - return NULL; + if (ownBuffer_->empty()) + { + return NULL; + } + else + { + return ownBuffer_->c_str(); + } } else { - return buffer_.GetConstContent().c_str(); + return bufferData_; } } @@ -256,12 +302,19 @@ { ComputeMissingInformation(); - if (!buffer_.HasContent()) + if (!hasBuffer_) { throw OrthancException(ErrorCode_InternalError); } - return buffer_.GetConstContent().size(); + if (ownBuffer_.get() != NULL) + { + return ownBuffer_->size(); + } + else + { + return bufferSize_; + } } @@ -326,6 +379,22 @@ return false; } + + + bool HasPixelData() + { + ComputeMissingInformation(); + ParseDicomFile(); + + if (parsed_.HasContent()) + { + return parsed_.GetContent().HasTag(DICOM_TAG_PIXEL_DATA); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } }; @@ -347,9 +416,10 @@ } - void DicomInstanceToStore::SetBuffer(const std::string& dicom) + void DicomInstanceToStore::SetBuffer(const void* dicom, + size_t size) { - pimpl_->buffer_.SetConstReference(dicom); + pimpl_->SetBuffer(dicom, size); } @@ -391,15 +461,15 @@ } - const char* DicomInstanceToStore::GetBufferData() + const void* DicomInstanceToStore::GetBufferData() const { - return pimpl_->GetBufferData(); + return const_cast(*pimpl_).GetBufferData(); } - size_t DicomInstanceToStore::GetBufferSize() + size_t DicomInstanceToStore::GetBufferSize() const { - return pimpl_->GetBufferSize(); + return const_cast(*pimpl_).GetBufferSize(); } @@ -409,15 +479,15 @@ } - const Json::Value& DicomInstanceToStore::GetJson() + const Json::Value& DicomInstanceToStore::GetJson() const { - return pimpl_->GetJson(); + return const_cast(*pimpl_).GetJson(); } - bool DicomInstanceToStore::LookupTransferSyntax(std::string& result) + bool DicomInstanceToStore::LookupTransferSyntax(std::string& result) const { - return pimpl_->LookupTransferSyntax(result); + return const_cast(*pimpl_).LookupTransferSyntax(result); } @@ -425,4 +495,9 @@ { return pimpl_->GetHasher(); } + + bool DicomInstanceToStore::HasPixelData() const + { + return const_cast(*pimpl_).HasPixelData(); + } } diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/DicomInstanceToStore.h --- a/OrthancServer/DicomInstanceToStore.h Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/DicomInstanceToStore.h Tue Apr 21 16:37:25 2020 +0200 @@ -44,7 +44,7 @@ { class ParsedDicomFile; - class DicomInstanceToStore + class DicomInstanceToStore : public boost::noncopyable { public: typedef std::map, std::string> MetadataMap; @@ -59,8 +59,11 @@ void SetOrigin(const DicomInstanceOrigin& origin); const DicomInstanceOrigin& GetOrigin() const; - - void SetBuffer(const std::string& dicom); + + // WARNING: The buffer is not copied, it must not be removed as + // long as the "DicomInstanceToStore" object is alive + void SetBuffer(const void* dicom, + size_t size); void SetParsedDicomFile(ParsedDicomFile& parsed); @@ -76,16 +79,18 @@ MetadataType metadata, const std::string& value); - const char* GetBufferData(); + const void* GetBufferData() const; - size_t GetBufferSize(); + size_t GetBufferSize() const; const DicomMap& GetSummary(); - const Json::Value& GetJson(); + const Json::Value& GetJson() const; - bool LookupTransferSyntax(std::string& result); + bool LookupTransferSyntax(std::string& result) const; DicomInstanceHasher& GetHasher(); + + bool HasPixelData() const; }; } diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/LuaScripting.cpp --- a/OrthancServer/LuaScripting.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/LuaScripting.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -772,6 +772,8 @@ LOG(ERROR) << "Error while processing Lua events: " << e.What(); } } + + that->jobManager_.GetDicomConnectionManager().CloseIfInactive(); } } @@ -874,6 +876,17 @@ instance.GetOrigin().Format(origin); call.PushJson(origin); + Json::Value info = Json::objectValue; + info["HasPixelData"] = instance.HasPixelData(); + + std::string s; + if (instance.LookupTransferSyntax(s)) + { + info["TransferSyntaxUID"] = s; + } + + call.PushJson(info); + if (!call.ExecutePredicate()) { return false; diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/LuaScripting.h --- a/OrthancServer/LuaScripting.h Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/LuaScripting.h Tue Apr 21 16:37:25 2020 +0200 @@ -136,5 +136,10 @@ void SignalJobSuccess(const std::string& jobId); void SignalJobFailure(const std::string& jobId); + + TimeoutDicomConnectionManager& GetDicomConnectionManager() + { + return jobManager_.GetDicomConnectionManager(); + } }; } diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -121,22 +121,23 @@ "Received an empty DICOM file"); } + // The lifetime of "dicom" must be longer than "toStore", as the + // latter can possibly store a reference to the former (*) std::string dicom; + DicomInstanceToStore toStore; + toStore.SetOrigin(DicomInstanceOrigin::FromRest(call)); + if (boost::iequals(call.GetHttpHeader("content-encoding", ""), "gzip")) { GzipCompressor compressor; compressor.Uncompress(dicom, call.GetBodyData(), call.GetBodySize()); + toStore.SetBuffer(dicom.c_str(), dicom.size()); // (*) } else { - // TODO Remove unneccessary memcpy - call.BodyToString(dicom); - } - - DicomInstanceToStore toStore; - toStore.SetOrigin(DicomInstanceOrigin::FromRest(call)); - toStore.SetBuffer(dicom); + toStore.SetBuffer(call.GetBodyData(), call.GetBodySize()); + } std::string publicId; StoreStatus status = context.Store(publicId, toStore); diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -35,6 +35,8 @@ #include "OrthancRestApi.h" #include "../../Core/Cache/SharedArchive.h" +#include "../../Core/DicomNetworking/DicomAssociation.h" +#include "../../Core/DicomNetworking/DicomControlUserConnection.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" @@ -80,8 +82,7 @@ try { - DicomUserConnection connection(localAet, remote); - connection.Open(); + DicomControlUserConnection connection(localAet, remote); if (connection.Echo()) { @@ -127,7 +128,7 @@ static void FindPatient(DicomFindAnswers& result, - DicomUserConnection& connection, + DicomControlUserConnection& connection, const DicomMap& fields) { // Only keep the filters from "fields" that are related to the patient @@ -138,7 +139,7 @@ static void FindStudy(DicomFindAnswers& result, - DicomUserConnection& connection, + DicomControlUserConnection& connection, const DicomMap& fields) { // Only keep the filters from "fields" that are related to the study @@ -153,7 +154,7 @@ } static void FindSeries(DicomFindAnswers& result, - DicomUserConnection& connection, + DicomControlUserConnection& connection, const DicomMap& fields) { // Only keep the filters from "fields" that are related to the series @@ -168,7 +169,7 @@ } static void FindInstance(DicomFindAnswers& result, - DicomUserConnection& connection, + DicomControlUserConnection& connection, const DicomMap& fields) { // Only keep the filters from "fields" that are related to the instance @@ -203,8 +204,7 @@ DicomFindAnswers answers(false); { - DicomUserConnection connection(localAet, remote); - connection.Open(); + DicomControlUserConnection connection(localAet, remote); FindPatient(answers, connection, fields); } @@ -238,8 +238,7 @@ DicomFindAnswers answers(false); { - DicomUserConnection connection(localAet, remote); - connection.Open(); + DicomControlUserConnection connection(localAet, remote); FindStudy(answers, connection, fields); } @@ -274,8 +273,7 @@ DicomFindAnswers answers(false); { - DicomUserConnection connection(localAet, remote); - connection.Open(); + DicomControlUserConnection connection(localAet, remote); FindSeries(answers, connection, fields); } @@ -311,8 +309,7 @@ DicomFindAnswers answers(false); { - DicomUserConnection connection(localAet, remote); - connection.Open(); + DicomControlUserConnection connection(localAet, remote); FindInstance(answers, connection, fields); } @@ -350,8 +347,7 @@ RemoteModalityParameters remote = MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomUserConnection connection(localAet, remote); - connection.Open(); + DicomControlUserConnection connection(localAet, remote); DicomFindAnswers patients(false); FindPatient(patients, connection, m); @@ -804,7 +800,7 @@ DicomMap answer; parent.GetHandler().GetAnswer(answer, index); - // This switch-case mimics "DicomUserConnection::Move()" + // This switch-case mimics "DicomControlUserConnection::Move()" switch (parent.GetHandler().GetLevel()) { case ResourceType_Patient: @@ -1031,8 +1027,7 @@ const RemoteModalityParameters source = MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomUserConnection connection(localAet, source); - connection.Open(); + DicomControlUserConnection connection(localAet, source); for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++) { @@ -1315,8 +1310,7 @@ DicomFindAnswers answers(true); { - DicomUserConnection connection(localAet, remote); - connection.Open(); + DicomControlUserConnection connection(localAet, remote); connection.FindWorklist(answers, *query); } @@ -1486,11 +1480,11 @@ context.GetStorageCommitmentReports().Store( transactionUid, new StorageCommitmentReports::Report(remoteAet)); - DicomUserConnection scu(localAet, remote); - + DicomAssociationParameters parameters(localAet, remote); + std::vector a(sopClassUids.begin(), sopClassUids.end()); std::vector b(sopInstanceUids.begin(), sopInstanceUids.end()); - scu.RequestStorageCommitment(transactionUid, a, b); + DicomAssociation::RequestStorageCommitment(parameters, transactionUid, a, b); } Json::Value result = Json::objectValue; diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -694,8 +694,8 @@ dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT); } - windowWidth = static_cast(1 << info.GetBitsStored()); - windowCenter = windowWidth / 2.0f; + windowWidth = static_cast(1 << info.GetBitsStored()) * rescaleSlope; + windowCenter = windowWidth / 2.0f + rescaleIntercept; if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) && dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH)) diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/QueryRetrieveHandler.cpp --- a/OrthancServer/QueryRetrieveHandler.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/QueryRetrieveHandler.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -36,6 +36,7 @@ #include "OrthancConfiguration.h" +#include "../Core/DicomNetworking/DicomControlUserConnection.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/Logging.h" #include "LuaScripting.h" @@ -81,8 +82,7 @@ FixQueryLua(fixed, context_, modality_.GetApplicationEntityTitle()); { - DicomUserConnection connection(localAet_, modality_); - connection.Open(); + DicomControlUserConnection connection(localAet_, modality_); connection.Find(answers_, level_, fixed, findNormalized_); } diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerContext.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -669,7 +669,7 @@ { #if ENABLE_DICOM_CACHE == 0 static std::unique_ptr p; - p.reset(provider_.Provide(instancePublicId)); + p.reset(that_.provider_.Provide(instancePublicId)); dicom_ = dynamic_cast(p.get()); #else dicom_ = &dynamic_cast(that_.dicomCache_.Access(instancePublicId)); diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/DicomModalityStoreJob.cpp --- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -47,9 +47,7 @@ { if (connection_.get() == NULL) { - connection_.reset(new DicomUserConnection); - connection_->SetLocalApplicationEntityTitle(localAet_); - connection_->SetRemoteModality(remote_); + connection_.reset(new DicomUserConnection(localAet_, remote_)); } } diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/DicomMoveScuJob.cpp --- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -96,8 +96,7 @@ { if (connection_.get() == NULL) { - connection_.reset(new DicomUserConnection(localAet_, remote_)); - connection_->Open(); + connection_.reset(new DicomControlUserConnection(localAet_, remote_)); } connection_->Move(targetAet_, findAnswer); diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/DicomMoveScuJob.h --- a/OrthancServer/ServerJobs/DicomMoveScuJob.h Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.h Tue Apr 21 16:37:25 2020 +0200 @@ -34,8 +34,8 @@ #pragma once #include "../../Core/Compatibility.h" +#include "../../Core/DicomNetworking/DicomControlUserConnection.h" #include "../../Core/JobsEngine/SetOfCommandsJob.h" -#include "../../Core/DicomNetworking/DicomUserConnection.h" #include "../QueryRetrieveHandler.h" @@ -49,13 +49,14 @@ class Command; class Unserializer; - ServerContext& context_; - std::string localAet_; - std::string targetAet_; - RemoteModalityParameters remote_; - std::unique_ptr connection_; - Json::Value query_; + ServerContext& context_; + std::string localAet_; + std::string targetAet_; + RemoteModalityParameters remote_; + Json::Value query_; + std::unique_ptr connection_; + void Retrieve(const DicomMap& findAnswer); public: diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/LuaJobManager.cpp --- a/OrthancServer/ServerJobs/LuaJobManager.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/LuaJobManager.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -68,13 +68,16 @@ priority_(0), trailingTimeout_(5000) { + unsigned int dicomTimeout; + { OrthancConfiguration::ReaderLock lock; - dicomTimeout_ = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay", 5); + dicomTimeout = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay", 5); } + connectionManager_.SetInactivityTimeout(dicomTimeout * 1000); // Milliseconds expected LOG(INFO) << "Lua: DICOM associations will be closed after " - << dicomTimeout_ << " seconds of inactivity"; + << dicomTimeout << " seconds of inactivity"; } @@ -149,7 +152,6 @@ { jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_)); jobLock_->SetTrailingOperationTimeout(that_.trailingTimeout_); - jobLock_->SetDicomAssociationTimeout(that_.dicomTimeout_ * 1000); // Milliseconds expected } } @@ -202,7 +204,7 @@ const RemoteModalityParameters& modality) { assert(jobLock_.get() != NULL); - return jobLock_->AddOperation(new StoreScuOperation(localAet, modality)); + return jobLock_->AddOperation(new StoreScuOperation(that_.connectionManager_, localAet, modality)); } diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/LuaJobManager.h --- a/OrthancServer/ServerJobs/LuaJobManager.h Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/LuaJobManager.h Tue Apr 21 16:37:25 2020 +0200 @@ -33,6 +33,7 @@ #pragma once +#include "../../Core/DicomNetworking/TimeoutDicomConnectionManager.h" #include "../../Core/DicomParsing/DicomModification.h" #include "../../Core/JobsEngine/JobsEngine.h" #include "../../Core/JobsEngine/Operations/SequenceOfOperationsJob.h" @@ -51,7 +52,7 @@ size_t maxOperations_; int priority_; unsigned int trailingTimeout_; - unsigned int dicomTimeout_; + TimeoutDicomConnectionManager connectionManager_; virtual void SignalDone(const SequenceOfOperationsJob& job); @@ -66,6 +67,11 @@ void AwakeTrailingSleep(); + TimeoutDicomConnectionManager& GetDicomConnectionManager() + { + return connectionManager_; + } + class Lock : public boost::noncopyable { private: diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp --- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -43,8 +43,7 @@ namespace Orthanc { void DeleteResourceOperation::Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager) + const JobOperationValue& input) { switch (input.GetType()) { diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h --- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h Tue Apr 21 16:37:25 2020 +0200 @@ -51,8 +51,7 @@ } virtual void Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager); + const JobOperationValue& input); virtual void Serialize(Json::Value& result) const { diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp --- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -81,8 +81,7 @@ } void ModifyInstanceOperation::Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager) + const JobOperationValue& input) { if (input.GetType() != JobOperationValue::Type_DicomInstance) { diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h --- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h Tue Apr 21 16:37:25 2020 +0200 @@ -67,8 +67,7 @@ } virtual void Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager); + const JobOperationValue& input); virtual void Serialize(Json::Value& target) const; }; diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp --- a/OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -44,8 +44,7 @@ namespace Orthanc { void StorePeerOperation::Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager) + const JobOperationValue& input) { // Configure the HTTP client HttpClient client(peer_, "instances"); diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/StorePeerOperation.h --- a/OrthancServer/ServerJobs/Operations/StorePeerOperation.h Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.h Tue Apr 21 16:37:25 2020 +0200 @@ -57,8 +57,7 @@ } virtual void Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager); + const JobOperationValue& input); virtual void Serialize(Json::Value& result) const; }; diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp --- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -43,18 +43,10 @@ namespace Orthanc { void StoreScuOperation::Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager) + const JobOperationValue& input) { - std::unique_ptr resource - (connectionManager.AcquireConnection(localAet_, modality_)); - - if (resource.get() == NULL) - { - LOG(ERROR) << "Lua: Cannot connect to modality: " << modality_.GetApplicationEntityTitle(); - return; - } - + TimeoutDicomConnectionManager::Lock lock(connectionManager_, localAet_, modality_); + if (input.GetType() != JobOperationValue::Type_DicomInstance) { throw OrthancException(ErrorCode_BadParameterType); @@ -72,7 +64,7 @@ instance.ReadDicom(dicom); std::string sopClassUid, sopInstanceUid; // Unused - resource->GetConnection().Store(sopClassUid, sopInstanceUid, dicom); + lock.GetConnection().Store(sopClassUid, sopInstanceUid, dicom); } catch (OrthancException& e) { @@ -93,7 +85,9 @@ } - StoreScuOperation::StoreScuOperation(const Json::Value& serialized) + StoreScuOperation::StoreScuOperation(TimeoutDicomConnectionManager& connectionManager, + const Json::Value& serialized) : + connectionManager_(connectionManager) { if (SerializationToolbox::ReadString(serialized, "Type") != "StoreScu" || !serialized.isMember("LocalAET")) diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/StoreScuOperation.h --- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.h Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.h Tue Apr 21 16:37:25 2020 +0200 @@ -34,25 +34,29 @@ #pragma once #include "../../../Core/JobsEngine/Operations/IJobOperation.h" -#include "../../../Core/DicomNetworking/RemoteModalityParameters.h" +#include "../../../Core/DicomNetworking/TimeoutDicomConnectionManager.h" namespace Orthanc { class StoreScuOperation : public IJobOperation { private: - std::string localAet_; - RemoteModalityParameters modality_; + TimeoutDicomConnectionManager& connectionManager_; + std::string localAet_; + RemoteModalityParameters modality_; public: - StoreScuOperation(const std::string& localAet, + StoreScuOperation(TimeoutDicomConnectionManager& connectionManager, + const std::string& localAet, const RemoteModalityParameters& modality) : + connectionManager_(connectionManager), localAet_(localAet), modality_(modality) { } - StoreScuOperation(const Json::Value& serialized); + StoreScuOperation(TimeoutDicomConnectionManager& connectionManager, + const Json::Value& serialized); const std::string& GetLocalAet() const { @@ -65,8 +69,7 @@ } virtual void Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& manager); + const JobOperationValue& input); virtual void Serialize(Json::Value& result) const; }; diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp --- a/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -74,8 +74,7 @@ void SystemCallOperation::Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager) + const JobOperationValue& input) { std::vector arguments = preArguments_; diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/Operations/SystemCallOperation.h --- a/OrthancServer/ServerJobs/Operations/SystemCallOperation.h Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.h Tue Apr 21 16:37:25 2020 +0200 @@ -93,8 +93,7 @@ const std::string& GetPostArgument(size_t i) const; virtual void Apply(JobOperationValues& outputs, - const JobOperationValue& input, - IDicomConnectionManager& connectionManager); + const JobOperationValue& input); virtual void Serialize(Json::Value& result) const; }; diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/OrthancJobUnserializer.cpp --- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -126,7 +126,8 @@ } else if (type == "StoreScu") { - return new StoreScuOperation(source); + return new StoreScuOperation( + context_.GetLuaScripting().GetDicomConnectionManager(), source); } else if (type == "SystemCall") { diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp --- a/OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -34,7 +34,7 @@ #include "../PrecompiledHeadersServer.h" #include "StorageCommitmentScpJob.h" -#include "../../Core/DicomNetworking/DicomUserConnection.h" +#include "../../Core/DicomNetworking/DicomAssociation.h" #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" #include "../../Core/SerializationToolbox.h" @@ -347,9 +347,10 @@ { throw OrthancException(ErrorCode_InternalError); } - - DicomUserConnection scu(calledAet_, remoteModality_); - scu.ReportStorageCommitment(transactionUid_, sopClassUids_, sopInstanceUids_, failureReasons); + + DicomAssociationParameters parameters(calledAet_, remoteModality_); + DicomAssociation::ReportStorageCommitment( + parameters, transactionUid_, sopClassUids_, sopInstanceUids_, failureReasons); } diff -r 4f78da5613a1 -r 3ab2d48c8f69 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/OrthancServer/main.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -83,7 +83,7 @@ DicomInstanceToStore toStore; toStore.SetOrigin(DicomInstanceOrigin::FromDicomProtocol (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str())); - toStore.SetBuffer(dicomFile); + toStore.SetBuffer(dicomFile.c_str(), dicomFile.size()); toStore.SetSummary(dicomSummary); toStore.SetJson(dicomJson); diff -r 4f78da5613a1 -r 3ab2d48c8f69 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/Plugins/Engine/OrthancPlugins.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -825,6 +825,7 @@ typedef std::list OnChangeCallbacks; typedef std::list IncomingHttpRequestFilters; typedef std::list IncomingHttpRequestFilters2; + typedef std::list IncomingDicomInstanceFilters; typedef std::list DecodeImageCallbacks; typedef std::list JobsUnserializers; typedef std::list RefreshMetricsCallbacks; @@ -844,6 +845,7 @@ _OrthancPluginMoveCallback moveCallbacks_; IncomingHttpRequestFilters incomingHttpRequestFilters_; IncomingHttpRequestFilters2 incomingHttpRequestFilters2_; + IncomingDicomInstanceFilters incomingDicomInstanceFilters_; RefreshMetricsCallbacks refreshMetricsCallbacks_; StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_; std::unique_ptr storageArea_; @@ -1782,7 +1784,33 @@ } - + bool OrthancPlugins::FilterIncomingInstance(const DicomInstanceToStore& instance, + const Json::Value& simplified) + { + boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); + + for (PImpl::IncomingDicomInstanceFilters::const_iterator + filter = pimpl_->incomingDicomInstanceFilters_.begin(); + filter != pimpl_->incomingDicomInstanceFilters_.end(); ++filter) + { + int32_t allowed = (*filter) ( + reinterpret_cast(&instance)); + + if (allowed == 0) + { + return false; + } + else if (allowed != 1) + { + // The callback is only allowed to answer 0 or 1 + throw OrthancException(ErrorCode_Plugin); + } + } + + return true; + } + + void OrthancPlugins::SignalChangeInternal(OrthancPluginChangeType changeType, OrthancPluginResourceType resourceType, const char* resource) @@ -1967,6 +1995,16 @@ } + void OrthancPlugins::RegisterIncomingDicomInstanceFilter(const void* parameters) + { + const _OrthancPluginIncomingDicomInstanceFilter& p = + *reinterpret_cast(parameters); + + LOG(INFO) << "Plugin has registered a callback to filter incoming DICOM instances"; + pimpl_->incomingDicomInstanceFilters_.push_back(p.callback); + } + + void OrthancPlugins::RegisterRefreshMetricsCallback(const void* parameters) { const _OrthancPluginRegisterRefreshMetricsCallback& p = @@ -2419,8 +2457,8 @@ const _OrthancPluginAccessDicomInstance& p = *reinterpret_cast(parameters); - DicomInstanceToStore& instance = - *reinterpret_cast(p.instance); + const DicomInstanceToStore& instance = + *reinterpret_cast(p.instance); switch (service) { @@ -2433,7 +2471,7 @@ return; case _OrthancPluginService_GetInstanceData: - *p.resultString = instance.GetBufferData(); + *p.resultString = reinterpret_cast(instance.GetBufferData()); return; case _OrthancPluginService_HasInstanceMetadata: @@ -2469,6 +2507,22 @@ *p.resultOrigin = Plugins::Convert(instance.GetOrigin().GetRequestOrigin()); return; + case _OrthancPluginService_GetInstanceTransferSyntaxUid: // New in Orthanc 1.6.1 + { + std::string s; + if (!instance.LookupTransferSyntax(s)) + { + s.clear(); + } + + *p.resultStringToFree = CopyString(s); + return; + } + + case _OrthancPluginService_HasInstancePixelData: // New in Orthanc 1.6.1 + *p.resultInt64 = instance.HasPixelData(); + return; + default: throw OrthancException(ErrorCode_InternalError); } @@ -3420,6 +3474,8 @@ case _OrthancPluginService_HasInstanceMetadata: case _OrthancPluginService_GetInstanceMetadata: case _OrthancPluginService_GetInstanceOrigin: + case _OrthancPluginService_GetInstanceTransferSyntaxUid: + case _OrthancPluginService_HasInstancePixelData: AccessDicomInstance(service, parameters); return true; @@ -4034,6 +4090,10 @@ RegisterIncomingHttpRequestFilter2(parameters); return true; + case _OrthancPluginService_RegisterIncomingDicomInstanceFilter: + RegisterIncomingDicomInstanceFilter(parameters); + return true; + case _OrthancPluginService_RegisterRefreshMetricsCallback: RegisterRefreshMetricsCallback(parameters); return true; @@ -4477,46 +4537,50 @@ getValues[i] = getArguments[i].second.c_str(); } - // Improved callback with support for GET arguments, since Orthanc 1.3.0 - for (PImpl::IncomingHttpRequestFilters2::const_iterator - filter = pimpl_->incomingHttpRequestFilters2_.begin(); - filter != pimpl_->incomingHttpRequestFilters2_.end(); ++filter) { - int32_t allowed = (*filter) (cMethod, uri, ip, - httpKeys.size(), - httpKeys.empty() ? NULL : &httpKeys[0], - httpValues.empty() ? NULL : &httpValues[0], - getKeys.size(), - getKeys.empty() ? NULL : &getKeys[0], - getValues.empty() ? NULL : &getValues[0]); - - if (allowed == 0) - { - return false; - } - else if (allowed != 1) + boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); + + // Improved callback with support for GET arguments, since Orthanc 1.3.0 + for (PImpl::IncomingHttpRequestFilters2::const_iterator + filter = pimpl_->incomingHttpRequestFilters2_.begin(); + filter != pimpl_->incomingHttpRequestFilters2_.end(); ++filter) { - // The callback is only allowed to answer 0 or 1 - throw OrthancException(ErrorCode_Plugin); + int32_t allowed = (*filter) (cMethod, uri, ip, + httpKeys.size(), + httpKeys.empty() ? NULL : &httpKeys[0], + httpValues.empty() ? NULL : &httpValues[0], + getKeys.size(), + getKeys.empty() ? NULL : &getKeys[0], + getValues.empty() ? NULL : &getValues[0]); + + if (allowed == 0) + { + return false; + } + else if (allowed != 1) + { + // The callback is only allowed to answer 0 or 1 + throw OrthancException(ErrorCode_Plugin); + } } - } - - for (PImpl::IncomingHttpRequestFilters::const_iterator - filter = pimpl_->incomingHttpRequestFilters_.begin(); - filter != pimpl_->incomingHttpRequestFilters_.end(); ++filter) - { - int32_t allowed = (*filter) (cMethod, uri, ip, httpKeys.size(), - httpKeys.empty() ? NULL : &httpKeys[0], - httpValues.empty() ? NULL : &httpValues[0]); - - if (allowed == 0) + + for (PImpl::IncomingHttpRequestFilters::const_iterator + filter = pimpl_->incomingHttpRequestFilters_.begin(); + filter != pimpl_->incomingHttpRequestFilters_.end(); ++filter) { - return false; - } - else if (allowed != 1) - { - // The callback is only allowed to answer 0 or 1 - throw OrthancException(ErrorCode_Plugin); + int32_t allowed = (*filter) (cMethod, uri, ip, httpKeys.size(), + httpKeys.empty() ? NULL : &httpKeys[0], + httpValues.empty() ? NULL : &httpValues[0]); + + if (allowed == 0) + { + return false; + } + else if (allowed != 1) + { + // The callback is only allowed to answer 0 or 1 + throw OrthancException(ErrorCode_Plugin); + } } } diff -r 4f78da5613a1 -r 3ab2d48c8f69 Plugins/Engine/OrthancPlugins.h --- a/Plugins/Engine/OrthancPlugins.h Fri Mar 27 10:06:58 2020 -0400 +++ b/Plugins/Engine/OrthancPlugins.h Tue Apr 21 16:37:25 2020 +0200 @@ -124,6 +124,8 @@ void RegisterIncomingHttpRequestFilter2(const void* parameters); + void RegisterIncomingDicomInstanceFilter(const void* parameters); + void RegisterRefreshMetricsCallback(const void* parameters); void RegisterStorageCommitmentScpCallback(const void* parameters); @@ -252,10 +254,7 @@ const Json::Value& simplifiedTags) ORTHANC_OVERRIDE; virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance, - const Json::Value& simplified) ORTHANC_OVERRIDE - { - return true; // TODO Enable filtering of instances from plugins - } + const Json::Value& simplified) ORTHANC_OVERRIDE; bool HasStorageArea() const; diff -r 4f78da5613a1 -r 3ab2d48c8f69 Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Fri Mar 27 10:06:58 2020 -0400 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Tue Apr 21 16:37:25 2020 +0200 @@ -27,6 +27,7 @@ * - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback(). * - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback(). * - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback(). + * - Possibly register a callback to filter incoming DICOM instance using OrthancPluginRegisterIncomingDicomInstanceFilter(). * -# void OrthancPluginFinalize(): * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -124,7 +125,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 6 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 0 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 1 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -179,7 +180,7 @@ /** * For Microsoft Visual Studio, a compatibility "stdint.h" can be * downloaded at the following URL: - * https://bitbucket.org/sjodogne/orthanc/raw/default/Resources/ThirdParty/VisualStudio/stdint.h + * https://hg.orthanc-server.com/orthanc/raw-file/tip/Resources/ThirdParty/VisualStudio/stdint.h **/ #include @@ -455,6 +456,7 @@ _OrthancPluginService_RegisterRefreshMetricsCallback = 1011, _OrthancPluginService_RegisterChunkedRestCallback = 1012, /* New in Orthanc 1.5.7 */ _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013, + _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014, /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -499,6 +501,8 @@ _OrthancPluginService_HasInstanceMetadata = 4005, _OrthancPluginService_GetInstanceMetadata = 4006, _OrthancPluginService_GetInstanceOrigin = 4007, + _OrthancPluginService_GetInstanceTransferSyntaxUid = 4008, + _OrthancPluginService_HasInstancePixelData = 4009, /* Services for plugins implementing a database back-end */ _OrthancPluginService_RegisterDatabaseBackend = 5000, @@ -1105,11 +1109,11 @@ /** - * @brief Signature of a callback function that is triggered when Orthanc receives a DICOM instance. + * @brief Signature of a callback function that is triggered when Orthanc stores a new DICOM instance. * @ingroup Callbacks **/ typedef OrthancPluginErrorCode (*OrthancPluginOnStoredInstanceCallback) ( - OrthancPluginDicomInstance* instance, + const OrthancPluginDicomInstance* instance, const char* instanceId); @@ -2694,12 +2698,12 @@ typedef struct { - char** resultStringToFree; - const char** resultString; - int64_t* resultInt64; - const char* key; - OrthancPluginDicomInstance* instance; - OrthancPluginInstanceOrigin* resultOrigin; /* New in Orthanc 0.9.5 SDK */ + char** resultStringToFree; + const char** resultString; + int64_t* resultInt64; + const char* key; + const OrthancPluginDicomInstance* instance; + OrthancPluginInstanceOrigin* resultOrigin; /* New in Orthanc 0.9.5 SDK */ } _OrthancPluginAccessDicomInstance; @@ -2715,8 +2719,8 @@ * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet( - OrthancPluginContext* context, - OrthancPluginDicomInstance* instance) + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) { const char* result; @@ -2748,8 +2752,8 @@ * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize( - OrthancPluginContext* context, - OrthancPluginDicomInstance* instance) + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) { int64_t size; @@ -2781,8 +2785,8 @@ * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData( - OrthancPluginContext* context, - OrthancPluginDicomInstance* instance) + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) { const char* result; @@ -2817,8 +2821,8 @@ * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson( - OrthancPluginContext* context, - OrthancPluginDicomInstance* instance) + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) { char* result; @@ -2855,8 +2859,8 @@ * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson( - OrthancPluginContext* context, - OrthancPluginDicomInstance* instance) + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) { char* result; @@ -2894,9 +2898,9 @@ * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE int OrthancPluginHasInstanceMetadata( - OrthancPluginContext* context, - OrthancPluginDicomInstance* instance, - const char* metadata) + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + const char* metadata) { int64_t result; @@ -2935,9 +2939,9 @@ * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata( - OrthancPluginContext* context, - OrthancPluginDicomInstance* instance, - const char* metadata) + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + const char* metadata) { const char* result; @@ -5107,8 +5111,8 @@ * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin( - OrthancPluginContext* context, - OrthancPluginDicomInstance* instance) + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) { OrthancPluginInstanceOrigin origin; @@ -7413,6 +7417,128 @@ return context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, ¶ms); } + + + /** + * @brief Callback to filter incoming DICOM instances received by Orthanc. + * + * Signature of a callback function that is triggered whenever + * Orthanc receives a new DICOM instance (e.g. through REST API or + * DICOM protocol), and that answers whether this DICOM instance + * should be accepted or discarded by Orthanc. + * + * Note that the metadata information is not available + * (i.e. GetInstanceMetadata() should not be used on "instance"). + * + * @param instance The received DICOM instance. + * @return 0 to discard the instance, 1 to store the instance, -1 if error. + * @ingroup Callback + **/ + typedef int32_t (*OrthancPluginIncomingDicomInstanceFilter) ( + const OrthancPluginDicomInstance* instance); + + + typedef struct + { + OrthancPluginIncomingDicomInstanceFilter callback; + } _OrthancPluginIncomingDicomInstanceFilter; + + /** + * @brief Register a callback to filter incoming DICOM instance. + * + * This function registers a custom callback to filter incoming + * DICOM instances received by Orthanc (either through the REST API + * or through the DICOM protocol). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingDicomInstanceFilter( + OrthancPluginContext* context, + OrthancPluginIncomingDicomInstanceFilter callback) + { + _OrthancPluginIncomingDicomInstanceFilter params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterIncomingDicomInstanceFilter, ¶ms); + } + + + /** + * @brief Get the transfer syntax of a DICOM file. + * + * This function returns a pointer to a newly created string that + * contains the transfer syntax UID of the DICOM instance. The empty + * string might be returned if this information is unknown. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The NULL value in case of error, or a string containing the + * transfer syntax UID. This string must be freed by OrthancPluginFreeString(). + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultStringToFree = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceTransferSyntaxUid, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Check whether the DICOM file has pixel data. + * + * This function returns a Boolean value indicating whether the + * DICOM instance contains the pixel data (7FE0,0010) tag. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return "1" if the DICOM instance contains pixel data, or "0" if + * the tag is missing, or "-1" in the case of an error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + int64_t hasPixelData; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultInt64 = &hasPixelData; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_HasInstancePixelData, ¶ms) != OrthancPluginErrorCode_Success || + hasPixelData < 0 || + hasPixelData > 1) + { + /* Error */ + return -1; + } + else + { + return hasPixelData; + } + } + + #ifdef __cplusplus } #endif diff -r 4f78da5613a1 -r 3ab2d48c8f69 Plugins/Samples/Basic/Plugin.c --- a/Plugins/Samples/Basic/Plugin.c Fri Mar 27 10:06:58 2020 -0400 +++ b/Plugins/Samples/Basic/Plugin.c Tue Apr 21 16:37:25 2020 +0200 @@ -29,9 +29,9 @@ static OrthancPluginErrorCode customError; -ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback1(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) +OrthancPluginErrorCode Callback1(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) { char buffer[1024]; uint32_t i; @@ -83,9 +83,9 @@ } -ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback2(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) +OrthancPluginErrorCode Callback2(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) { /* Answer with a sample 16bpp image. */ @@ -115,9 +115,9 @@ } -ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback3(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) +OrthancPluginErrorCode Callback3(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) { if (request->method != OrthancPluginHttpMethod_Get) { @@ -140,9 +140,9 @@ } -ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback4(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) +OrthancPluginErrorCode Callback4(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) { /* Answer with a sample 8bpp image. */ @@ -172,9 +172,9 @@ } -ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback5(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) +OrthancPluginErrorCode Callback5(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) { /** * Demonstration the difference between the @@ -222,9 +222,9 @@ } -ORTHANC_PLUGINS_API OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) +OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) { const char* pathLocator = "\"Path\" : \""; char info[1024]; @@ -266,7 +266,7 @@ } -ORTHANC_PLUGINS_API void DicomWebBinaryCallback( +void DicomWebBinaryCallback( OrthancPluginDicomWebNode* node, OrthancPluginDicomWebSetBinaryNode setter, uint32_t levelDepth, @@ -281,8 +281,8 @@ } -ORTHANC_PLUGINS_API OrthancPluginErrorCode OnStoredCallback(OrthancPluginDicomInstance* instance, - const char* instanceId) +OrthancPluginErrorCode OnStoredCallback(const OrthancPluginDicomInstance* instance, + const char* instanceId) { char buffer[256]; FILE* fp; @@ -333,9 +333,9 @@ } -ORTHANC_PLUGINS_API OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, - OrthancPluginResourceType resourceType, - const char* resourceId) +OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId) { char info[1024]; @@ -391,12 +391,12 @@ } -ORTHANC_PLUGINS_API int32_t FilterIncomingHttpRequest(OrthancPluginHttpMethod method, - const char* uri, - const char* ip, - uint32_t headersCount, - const char* const* headersKeys, - const char* const* headersValues) +int32_t FilterIncomingHttpRequest(OrthancPluginHttpMethod method, + const char* uri, + const char* ip, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues) { uint32_t i; @@ -423,11 +423,31 @@ } -ORTHANC_PLUGINS_API void RefreshMetrics() +static void RefreshMetrics() { static unsigned int count = 0; OrthancPluginSetMetricsValue(context, "sample_counter", - (float) (count++), OrthancPluginMetricsType_Default); + (float) (count++), OrthancPluginMetricsType_Default); +} + + +static int32_t FilterIncomingDicomInstance(const OrthancPluginDicomInstance* instance) +{ + char buf[1024]; + char* s; + int32_t hasPixelData; + + s = OrthancPluginGetInstanceTransferSyntaxUid(context, instance); + sprintf(buf, "Incoming transfer syntax: %s", s); + OrthancPluginFreeString(context, s); + OrthancPluginLogWarning(context, buf); + + hasPixelData = OrthancPluginHasInstancePixelData(context, instance); + sprintf(buf, "Incoming has pixel data: %d", hasPixelData); + OrthancPluginLogWarning(context, buf); + + /* Reject all instances without pixel data */ + return hasPixelData; } @@ -495,7 +515,8 @@ OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterIncomingHttpRequest); OrthancPluginRegisterRefreshMetricsCallback(context, RefreshMetrics); - + OrthancPluginRegisterIncomingDicomInstanceFilter(context, FilterIncomingDicomInstance); + /* Declare several properties of the plugin */ OrthancPluginSetRootUri(context, "/plugin/hello"); diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CMake/Compiler.cmake --- a/Resources/CMake/Compiler.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/CMake/Compiler.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -1,6 +1,7 @@ # This file sets all the compiler-related flags -if (CMAKE_CROSSCOMPILING OR +if ((CMAKE_CROSSCOMPILING AND NOT + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") # Cross-compilation necessarily implies standalone and static build SET(STATIC_BUILD ON) diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CMake/DcmtkConfiguration.cmake --- a/Resources/CMake/DcmtkConfiguration.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/CMake/DcmtkConfiguration.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -133,13 +133,34 @@ else() - # The following line allows to manually add libraries at the - # command-line, which is necessary for Ubuntu/Debian packages - set(tmp "${DCMTK_LIBRARIES}") - include(FindDCMTK) - list(APPEND DCMTK_LIBRARIES "${tmp}") + if (CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + + CHECK_INCLUDE_FILE_CXX(dcmtk/dcmdata/dcfilefo.h HAVE_DCMTK_H) + if (NOT HAVE_DCMTK_H) + message(FATAL_ERROR "Please install the libdcmtk-dev package") + endif() + + CHECK_LIBRARY_EXISTS(dcmdata "dcmDataDict" "" HAVE_DCMTK_LIB) + if (NOT HAVE_DCMTK_LIB) + message(FATAL_ERROR "Please install the libdcmtk package") + endif() - include_directories(${DCMTK_INCLUDE_DIRS}) + find_path(DCMTK_INCLUDE_DIRS dcmtk/config/osconfig.h + /usr/include + ) + + link_libraries(dcmdata dcmnet dcmjpeg oflog ofstd) + + else() + # The following line allows to manually add libraries at the + # command-line, which is necessary for Ubuntu/Debian packages + set(tmp "${DCMTK_LIBRARIES}") + include(FindDCMTK) + list(APPEND DCMTK_LIBRARIES "${tmp}") + + include_directories(${DCMTK_INCLUDE_DIRS}) + endif() add_definitions( -DHAVE_CONFIG_H=1 @@ -210,6 +231,13 @@ message(FATAL_ERROR "Cannot locate the DICOM dictionary on this system") endif() + if (CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + # Remove the sysroot prefix + file(RELATIVE_PATH tmp ${CMAKE_FIND_ROOT_PATH} ${DCMTK_DICTIONARY_DIR_AUTO}) + set(DCMTK_DICTIONARY_DIR_AUTO /${tmp} CACHE INTERNAL "") + endif() + message("Autodetected path to the DICOM dictionaries: ${DCMTK_DICTIONARY_DIR_AUTO}") add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR_AUTO}") else() diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CMake/GoogleTestConfiguration.cmake --- a/Resources/CMake/GoogleTestConfiguration.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/CMake/GoogleTestConfiguration.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -2,15 +2,15 @@ find_path(GOOGLE_TEST_DEBIAN_SOURCES_DIR NAMES src/gtest-all.cc PATHS - /usr/src/gtest - /usr/src/googletest/googletest + ${CROSSTOOL_NG_IMAGE}/usr/src/gtest + ${CROSSTOOL_NG_IMAGE}/usr/src/googletest/googletest PATH_SUFFIXES src ) find_path(GOOGLE_TEST_DEBIAN_INCLUDE_DIR NAMES gtest.h PATHS - /usr/include/gtest + ${CROSSTOOL_NG_IMAGE}/usr/include/gtest ) message("Path to the Debian Google Test sources: ${GOOGLE_TEST_DEBIAN_SOURCES_DIR}") diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CMake/LibCurlConfiguration.cmake --- a/Resources/CMake/LibCurlConfiguration.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/CMake/LibCurlConfiguration.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -262,7 +262,7 @@ check_struct_has_member("struct sockaddr_un" sun_path "sys/un.h" USE_UNIX_SOCKETS) - set(CMAKE_REQUIRED_INCLUDES "${CURL_SOURCES_DIR}/include") + list(APPEND CMAKE_REQUIRED_INCLUDES "${CURL_SOURCES_DIR}/include") set(CMAKE_EXTRA_INCLUDE_FILES "curl/system.h") check_type_size("curl_off_t" SIZEOF_CURL_OFF_T) @@ -312,6 +312,22 @@ ${CURL_SOURCES_DIR}/lib/curl_config.h ) endif() + +elseif (CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + + CHECK_INCLUDE_FILE_CXX(curl/curl.h HAVE_CURL_H) + if (NOT HAVE_CURL_H) + message(FATAL_ERROR "Please install the libcurl-dev package") + endif() + + CHECK_LIBRARY_EXISTS(curl "curl_easy_init" "" HAVE_CURL_LIB) + if (NOT HAVE_CURL_LIB) + message(FATAL_ERROR "Please install the libcurl package") + endif() + + link_libraries(curl) + else() include(FindCURL) include_directories(${CURL_INCLUDE_DIRS}) diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CMake/LuaConfiguration.cmake --- a/Resources/CMake/LuaConfiguration.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/CMake/LuaConfiguration.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -100,6 +100,32 @@ source_group(ThirdParty\\Lua REGULAR_EXPRESSION ${LUA_SOURCES_DIR}/.*) +elseif (CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + + set(LUA_VERSIONS 5.3 5.2 5.1) + + unset(LUA_VERSION) + foreach(version IN ITEMS ${LUA_VERSIONS}) + CHECK_INCLUDE_FILE(lua${version}/lua.h HAVE_LUA${version}_H) + if (HAVE_LUA${version}_H) + set(LUA_VERSION ${version}) + break() + endif() + endforeach() + + if (NOT LUA_VERSION) + message(FATAL_ERROR "Please install the liblua-dev package") + endif() + + CHECK_LIBRARY_EXISTS(lua${LUA_VERSION} "lua_call" "${LUA_LIB_DIR}" HAVE_LUA_LIB) + if (NOT HAVE_LUA_LIB) + message(FATAL_ERROR "Please install the liblua package") + endif() + + include_directories(${CROSSTOOL_NG_IMAGE}/usr/include/lua${LUA_VERSION}) + link_libraries(lua${LUA_VERSION}) + else() include(FindLua) diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CMake/OpenSslConfiguration.cmake --- a/Resources/CMake/OpenSslConfiguration.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/CMake/OpenSslConfiguration.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -9,6 +9,26 @@ source_group(ThirdParty\\OpenSSL REGULAR_EXPRESSION ${OPENSSL_SOURCES_DIR}/.*) +elseif (CMAKE_CROSSCOMPILING AND + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") + + CHECK_INCLUDE_FILE_CXX(openssl/opensslv.h HAVE_OPENSSL_H) + if (NOT HAVE_OPENSSL_H) + message(FATAL_ERROR "Please install the libopenssl-dev package") + endif() + + CHECK_LIBRARY_EXISTS(crypto "OPENSSL_init" "" HAVE_OPENSSL_CRYPTO_LIB) + if (NOT HAVE_OPENSSL_CRYPTO_LIB) + message(FATAL_ERROR "Please install the libopenssl package") + endif() + + CHECK_LIBRARY_EXISTS(ssl "SSL_library_init" "" HAVE_OPENSSL_SSL_LIB) + if (NOT HAVE_OPENSSL_SSL_LIB) + message(FATAL_ERROR "Please install the libopenssl package") + endif() + + link_libraries(crypto ssl) + else() include(FindOpenSSL) diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Tue Apr 21 16:37:25 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 diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CMake/OrthancFrameworkParameters.cmake diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CMake/SQLiteConfiguration.cmake --- a/Resources/CMake/SQLiteConfiguration.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/CMake/SQLiteConfiguration.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -41,12 +41,14 @@ source_group(ThirdParty\\SQLite REGULAR_EXPRESSION ${SQLITE_SOURCES_DIR}/.*) else() - CHECK_INCLUDE_FILE_CXX(sqlite3.h HAVE_SQLITE_H) + CHECK_INCLUDE_FILE(sqlite3.h HAVE_SQLITE_H) if (NOT HAVE_SQLITE_H) message(FATAL_ERROR "Please install the libsqlite3-dev package") endif() - find_path(SQLITE_INCLUDE_DIR sqlite3.h + find_path(SQLITE_INCLUDE_DIR + NAMES sqlite3.h + PATHS /usr/include /usr/local/include ) diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/Configuration.json --- a/Resources/Configuration.json Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/Configuration.json Tue Apr 21 16:37:25 2020 +0200 @@ -409,7 +409,7 @@ // as new DICOM commands are issued. This option sets the number of // seconds of inactivity to wait before automatically closing a // DICOM association used by Lua. If set to 0, the connection is - // closed immediately. + // closed immediately. This option is only used in Lua scripts. "DicomAssociationCloseDelay" : 5, // Maximum number of query/retrieve DICOM requests that are diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/CrossToolchain.cmake --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/CrossToolchain.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -0,0 +1,56 @@ +# +# $ CROSSTOOL_NG_ARCH=mips CROSSTOOL_NG_BOARD=malta CROSSTOOL_NG_IMAGE=/tmp/mips cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/CrossToolchain.cmake -DBUILD_CONNECTIVITY_CHECKS=OFF -DUSE_SYSTEM_CIVETWEB=OFF -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON -DUSE_SYSTEM_JSONCPP=OFF -DUSE_SYSTEM_UUID=OFF -DENABLE_DCMTK_JPEG_LOSSLESS=OFF -G Ninja && ninja +# + +INCLUDE(CMakeForceCompiler) + +SET(CROSSTOOL_NG_ROOT $ENV{CROSSTOOL_NG_ROOT} CACHE STRING "") +SET(CROSSTOOL_NG_ARCH $ENV{CROSSTOOL_NG_ARCH} CACHE STRING "") +SET(CROSSTOOL_NG_BOARD $ENV{CROSSTOOL_NG_BOARD} CACHE STRING "") +SET(CROSSTOOL_NG_SUFFIX $ENV{CROSSTOOL_NG_SUFFIX} CACHE STRING "") +SET(CROSSTOOL_NG_IMAGE $ENV{CROSSTOOL_NG_IMAGE} CACHE STRING "") + +IF ("${CROSSTOOL_NG_ROOT}" STREQUAL "") + SET(CROSSTOOL_NG_ROOT "/home/$ENV{USER}/x-tools") +ENDIF() + +IF ("${CROSSTOOL_NG_SUFFIX}" STREQUAL "") + SET(CROSSTOOL_NG_SUFFIX "linux-gnu") +ENDIF() + +SET(CROSSTOOL_NG_NAME ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_BOARD}-${CROSSTOOL_NG_SUFFIX}) +SET(CROSSTOOL_NG_BASE ${CROSSTOOL_NG_ROOT}/${CROSSTOOL_NG_NAME}) + +# the name of the target operating system +SET(CMAKE_SYSTEM_NAME Linux) +SET(CMAKE_SYSTEM_VERSION CrossToolNg) +SET(CMAKE_SYSTEM_PROCESSOR ${CROSSTOOL_NG_ARCH}) + +# which compilers to use for C and C++ +SET(CMAKE_C_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-gcc) + +if (${CMAKE_VERSION} VERSION_LESS "3.6.0") + CMAKE_FORCE_CXX_COMPILER(${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++ GNU) +else() + SET(CMAKE_CXX_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++) +endif() + +# here is the target environment located +SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_IMAGE}) +#SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot) + +# adjust the default behaviour of the FIND_XXX() commands: +# search headers and libraries in the target environment, search +# programs in the host environment +SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +SET(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +SET(CMAKE_CROSSCOMPILING ON) +#SET(CROSS_COMPILER_PREFIX ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}) + +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE) +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE) +SET(CMAKE_EXE_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE) +SET(CMAKE_SHARED_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE) diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/DicomConformanceStatement.txt --- a/Resources/DicomConformanceStatement.txt Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/DicomConformanceStatement.txt Tue Apr 21 16:37:25 2020 +0200 @@ -257,8 +257,10 @@ The information above about the SCP support is readily extracted from the function "Orthanc::Internals::AcceptAssociation()" from file -"OrthancServer/Internals/CommandDispatcher.cpp". +"Core/DicomNetworking/Internals/CommandDispatcher.cpp". -The information above about the SCU support is derived from the class -"Orthanc::DicomUserConnection" from file -"OrthancServer/DicomProtocol/DicomUserConnection.cpp". +The information above about the SCU support is derived from the +classes "Orthanc::DicomControlUserConnection" and +"Orthanc::DicomStoreUserConnection" from file +"Core/DicomNetworking/DicomControlUserConnection.cpp" and +"Core/DicomNetworking/DicomStoreUserConnection.cpp". diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/DownloadOrthancFramework.cmake --- a/Resources/DownloadOrthancFramework.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/DownloadOrthancFramework.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -114,6 +114,8 @@ set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0") set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.1") + set(ORTHANC_FRAMEWORK_MD5 "3971f5de96ba71dc9d3f3690afeaa7c0") # Below this point are development snapshots that were used to # release some plugin, before an official release of the Orthanc @@ -216,7 +218,7 @@ else() message("Forking the Orthanc source repository using Mercurial") execute_process( - COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://bitbucket.org/sjodogne/orthanc" + COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://hg.orthanc-server.com/orthanc/" WORKING_DIRECTORY ${CMAKE_BINARY_DIR} RESULT_VARIABLE Failure ) diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/LinuxStandardBaseToolchain.cmake --- a/Resources/LinuxStandardBaseToolchain.cmake Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/LinuxStandardBaseToolchain.cmake Tue Apr 21 16:37:25 2020 +0200 @@ -10,10 +10,10 @@ INCLUDE(CMakeForceCompiler) -SET(LSB_PATH $ENV{LSB_PATH}) -SET(LSB_CC $ENV{LSB_CC}) -SET(LSB_CXX $ENV{LSB_CXX}) -SET(LSB_TARGET_VERSION "4.0") +SET(LSB_PATH $ENV{LSB_PATH} CACHE STRING "") +SET(LSB_CC $ENV{LSB_CC} CACHE STRING "") +SET(LSB_CXX $ENV{LSB_CXX} CACHE STRING "") +SET(LSB_TARGET_VERSION "4.0" CACHE STRING "") IF ("${LSB_PATH}" STREQUAL "") SET(LSB_PATH "/opt/lsb") diff -r 4f78da5613a1 -r 3ab2d48c8f69 Resources/Samples/README.txt --- a/Resources/Samples/README.txt Fri Mar 27 10:06:58 2020 -0400 +++ b/Resources/Samples/README.txt Tue Apr 21 16:37:25 2020 +0200 @@ -2,6 +2,6 @@ the "OrthancContributed" repository on GitHub: https://github.com/jodogne/OrthancContributed -The integration tests of Orthanc provide a lot of samples about the +The integration tests of Orthanc provide many samples about the features of the REST API of Orthanc: -https://bitbucket.org/sjodogne/orthanc-tests/src/default/Tests/Tests.py +https://hg.orthanc-server.com/orthanc-tests/file/tip/Tests/Tests.py diff -r 4f78da5613a1 -r 3ab2d48c8f69 UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/UnitTestsSources/FromDcmtkTests.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -710,7 +710,14 @@ TEST(ParsedDicomFile, ToJsonFlags2) { ParsedDicomFile f(true); - f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false, ""); + + { + // "ParsedDicomFile" uses Little Endian => 'B' (least significant + // byte) will be stored first in the memory buffer and in the + // file, then 'A'. Hence the expected "BA" value below. + Uint16 v[] = { 'A' * 256 + 'B', 0 }; + ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint16Array(DCM_PixelData, v, 2).good()); + } Json::Value v; f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0); @@ -729,7 +736,7 @@ ASSERT_EQ(6u, v.getMemberNames().size()); ASSERT_TRUE(v.isMember("7fe0,0010")); ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type()); - ASSERT_EQ("Pixels", v["7fe0,0010"].asString()); + ASSERT_EQ("BA", v["7fe0,0010"].asString().substr(0, 2)); f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0); ASSERT_EQ(Json::objectValue, v.type()); @@ -739,7 +746,7 @@ std::string mime, content; ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString())); ASSERT_EQ("application/octet-stream", mime); - ASSERT_EQ("Pixels", content); + ASSERT_EQ("BA", content.substr(0, 2)); } diff -r 4f78da5613a1 -r 3ab2d48c8f69 UnitTestsSources/ImageTests.cpp --- a/UnitTestsSources/ImageTests.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/UnitTestsSources/ImageTests.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -410,6 +410,28 @@ } { + // true means "enforce alignment by using a temporary buffer" + Orthanc::PamReader r(true); + r.ReadFromMemory(s); + + ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r.GetWidth(), width); + ASSERT_EQ(r.GetHeight(), height); + + v = 0; + for (unsigned int y = 0; y < height; y++) + { + const uint16_t* p = reinterpret_cast + ((const uint8_t*)r.GetConstBuffer() + y * r.GetPitch()); + ASSERT_EQ(p, r.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(v, *p); + } + } + } + + { Orthanc::TemporaryFile tmp; tmp.Write(s); @@ -432,4 +454,30 @@ } } } + + { + Orthanc::TemporaryFile tmp; + tmp.Write(s); + + // true means "enforce alignment by using a temporary buffer" + Orthanc::PamReader r2(true); + r2.ReadFromFile(tmp.GetPath()); + + ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r2.GetWidth(), width); + ASSERT_EQ(r2.GetHeight(), height); + + v = 0; + for (unsigned int y = 0; y < height; y++) + { + const uint16_t* p = reinterpret_cast + ((const uint8_t*)r2.GetConstBuffer() + y * r2.GetPitch()); + ASSERT_EQ(p, r2.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(*p, v); + } + } + } + } diff -r 4f78da5613a1 -r 3ab2d48c8f69 UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/UnitTestsSources/MultiThreadingTests.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -1098,7 +1098,6 @@ StringOperationValue s2("world"); lock.AddInput(a, s1); lock.AddInput(a, s2); - lock.SetDicomAssociationTimeout(200); lock.SetTrailingOperationTimeout(300); } @@ -1284,7 +1283,6 @@ MemoryStorageArea storage_; SQLiteDatabaseWrapper db_; // The SQLite DB is in memory std::unique_ptr context_; - TimeoutDicomConnectionManager manager_; public: OrthancJobsSerialization() @@ -1407,27 +1405,31 @@ // StoreScuOperation { - RemoteModalityParameters modality; - modality.SetApplicationEntityTitle("REMOTE"); - modality.SetHost("192.168.1.1"); - modality.SetPortNumber(1000); - modality.SetManufacturer(ModalityManufacturer_StoreScp); + TimeoutDicomConnectionManager luaManager; + + { + RemoteModalityParameters modality; + modality.SetApplicationEntityTitle("REMOTE"); + modality.SetHost("192.168.1.1"); + modality.SetPortNumber(1000); + modality.SetManufacturer(ModalityManufacturer_StoreScp); - StoreScuOperation operation("TEST", modality); + StoreScuOperation operation(luaManager, "TEST", modality); - ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); - operation.Serialize(s); - } + ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); + operation.Serialize(s); + } - { - operation.reset(unserializer.UnserializeOperation(s)); + { + operation.reset(unserializer.UnserializeOperation(s)); - const StoreScuOperation& tmp = dynamic_cast(*operation); - ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle()); - ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost()); - ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber()); - ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer()); - ASSERT_EQ("TEST", tmp.GetLocalAet()); + const StoreScuOperation& tmp = dynamic_cast(*operation); + ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost()); + ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer()); + ASSERT_EQ("TEST", tmp.GetLocalAet()); + } } // SystemCallOperation diff -r 4f78da5613a1 -r 3ab2d48c8f69 UnitTestsSources/RestApiTests.cpp --- a/UnitTestsSources/RestApiTests.cpp Fri Mar 27 10:06:58 2020 -0400 +++ b/UnitTestsSources/RestApiTests.cpp Tue Apr 21 16:37:25 2020 +0200 @@ -121,22 +121,26 @@ HttpClient c; c.SetHttpsVerifyPeers(true); c.SetHttpsCACertificates("UnitTestsResults/bitbucket.cert"); - c.SetUrl("https://bitbucket.org/sjodogne/orthanc/raw/Orthanc-0.9.3/Resources/Configuration.json"); + + // Test file modified on 2020-04-20, in order to use a git + // repository on BitBucket instead of a Mercurial repository + // (because Mercurial support disappears on 2020-05-31) + c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json"); Json::Value v; c.Apply(v); - ASSERT_TRUE(v.isMember("LuaScripts")); + ASSERT_TRUE(v.isMember("ServeFolders")); } TEST(HttpClient, SslNoVerification) { HttpClient c; c.SetHttpsVerifyPeers(false); - c.SetUrl("https://bitbucket.org/sjodogne/orthanc/raw/Orthanc-0.9.3/Resources/Configuration.json"); + c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json"); Json::Value v; c.Apply(v); - ASSERT_TRUE(v.isMember("LuaScripts")); + ASSERT_TRUE(v.isMember("ServeFolders")); } #endif