Mercurial > hg > orthanc
changeset 5960:e3945472797d get-scu
merged default -> get-scu
line wrap: on
line diff
--- a/NEWS Thu Jan 16 15:49:50 2025 +0100 +++ b/NEWS Thu Jan 16 16:40:08 2025 +0100 @@ -4,6 +4,14 @@ General ------- +* DICOM: + - Added support for C-GET SCU. + - Added a configuration "AcceptedSopClasses" and "RejectedSopClasses" to limit + the SOP classes accepted by Orthanc when acting as C-STORE SCP. + - New config option "DicomDefaultRetrieveMethod" to define wheter Orthanc uses C-MOVE or C-GET + to retrieve a resource after a C-Find (when calling /queries/.../retrieve). + This configuration can be overriden for each modality in "DicomModalities->..->RetrieveMethod". + Default value: "C-MOVE" to keep the backward compatibility. * New configuration options: - "MaximumConcurrentDcmtkTranscoders" to reduce CPU and memory usage by limiting the number of concurrent DCMTK transcoders that are simultaneously running @@ -90,12 +98,36 @@ https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html This has no impact on the Stone Web viewer and OHIF: https://discourse.orthanc-server.org/t/dicomwebplugin-does-not-return-series-metadata-properly/5195 - +* /queries/../retrieve now accepts a new field in the payload: "RetrieveMethod" to define wheter + Orthanc uses C-MOVE or C-GET to retrieve the resource. +* improved progress reporting of DicomMoveScu jobs. Maintenance ----------- * DICOM TLS: "DicomTlsTrustedCertificates" is not required anymore when issuing +<<<<<<< working copy + an outgoing SCU connexion when "DicomTlsRemoteCertificateRequired" is set to false. +* DICOM negotiation: + - When opening a DICOM SCU connection, Orthanc now only proposes the contexts that it is + going to use in the connection and not all contexts as in previous versions. E.g, when + performing a C-ECHO, Orthanc will not propose C-MOVE or C-FIND. +* DICOM Get-SCP: Orthanc won't refuse anymore to send e.g. a LittleEndianExplicit file when + the accepted transfer syntax is a compressed one. +* Introduced a new thread to update the statistics at regular interval for the + DB plugins that are implementing the UpdateAndGetStatistics function (currently only + PostgreSQL). This avoids very long update times in case you don't call /statistics + for a long period. +* Fix C-Find queries not returning computed tags like ModalitiesInStudy, NumberOfStudyRelatedSeries, ... + in very specific use-cases. +* Fix C-Find queries not returning private tags in the modality worklist plugin. +* Fix extremely rare error when 2 threads are trying to create the same folder in the File Storage + at the same time. +* Fix crashes if handling very large images. +* Fix deadlock when parsing specific invalid DICOM files. +* Loading plugins: + - Orthanc will now fail to start when provided with a plugin path that can not be found. +======= an outgoing SCU connection if "DicomTlsRemoteCertificateRequired" is set to "false" * Fix C-Find queries not returning computed tags such as ModalitiesInStudy, NumberOfStudyRelatedSeries,... in very specific use cases @@ -106,6 +138,7 @@ * Fix deadlock when parsing specific invalid DICOM files * Loading plugins: Orthanc will now fail to start when provided with a plugin path that can not be found +>>>>>>> merge rev * Metrics: - Fix a few metrics that were not published - Added 2 metrics: "orthanc_storage_cache_miss_count" and "orthanc_storage_cache_hit_count"
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Thu Jan 16 16:40:08 2025 +0100 @@ -607,6 +607,11 @@ "Name": "NoCGetHandler", "Description": "No request handler factory for DICOM C-GET SCP" }, + { + "Code": 2045, + "Name": "DicomGetUnavailable", + "Description": "DicomUserConnection: The C-GET command is not supported by the remote SCP" + },
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -1333,7 +1333,22 @@ return value->CopyToString(result, allowBinary); } } - + + bool DicomMap::LookupStringValues(std::set<std::string>& results, + const DicomTag& tag, + bool allowBinary) const + { + std::string tmp; + if (LookupStringValue(tmp, tag, allowBinary)) + { + Toolbox::SplitString(results, tmp, '\\'); + return true; + } + + return false; + } + + bool DicomMap::ParseInteger32(int32_t& result, const DicomTag& tag) const {
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Thu Jan 16 16:40:08 2025 +0100 @@ -181,7 +181,11 @@ bool LookupStringValue(std::string& result, const DicomTag& tag, bool allowBinary) const; - + + bool LookupStringValues(std::set<std::string>& results, + const DicomTag& tag, + bool allowBinary) const; + bool ParseInteger32(int32_t& result, const DicomTag& tag) const;
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -173,7 +173,6 @@ DicomAssociation::DicomAssociation() { - role_ = DicomAssociationRole_Default; isOpen_ = false; net_ = NULL; params_ = NULL; @@ -198,16 +197,6 @@ } - void DicomAssociation::SetRole(DicomAssociationRole role) - { - if (role_ != role) - { - Close(); - role_ = role; - } - } - - void DicomAssociation::ClearPresentationContexts() { Close(); @@ -215,7 +204,26 @@ proposed_.reserve(MAX_PROPOSED_PRESENTATIONS); } - + + static T_ASC_SC_ROLE GetDcmtkRole(DicomAssociationRole role) + { + switch (role) + { + case DicomAssociationRole_Default: + return ASC_SC_ROLE_DEFAULT; + + case DicomAssociationRole_Scu: + return ASC_SC_ROLE_SCU; + + case DicomAssociationRole_Scp: + return ASC_SC_ROLE_SCP; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + void DicomAssociation::Open(const DicomAssociationParameters& parameters) { if (isOpen_) @@ -240,24 +248,6 @@ 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 && @@ -291,7 +281,12 @@ "no timeout") << ")"; CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_)); +#if DCMTK_VERSION_NUMBER >= 368 + CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, parameters.GetMaximumPduLength(), acseTimeout)); +#else + // from 3.6.8, this version is obsolete CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, parameters.GetMaximumPduLength())); +#endif #if ORTHANC_ENABLE_SSL == 1 if (parameters.GetRemoteModality().IsDicomTlsEnabled()) @@ -351,12 +346,12 @@ assert(presentationContextId <= 255); const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str(); - const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_; + const std::list<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_; std::vector<const char*> transferSyntaxes; transferSyntaxes.reserve(source.size()); - for (std::set<DicomTransferSyntax>::const_iterator + for (std::list<DicomTransferSyntax>::const_iterator it = source.begin(); it != source.end(); ++it) { transferSyntaxes.push_back(GetTransferSyntaxUid(*it)); @@ -365,7 +360,7 @@ assert(!transferSyntaxes.empty()); CheckConnecting(parameters, ASC_addPresentationContext( params_, presentationContextId, abstractSyntax, - &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole)); + &transferSyntaxes[0], transferSyntaxes.size(), GetDcmtkRole(proposed_[i].role_))); presentationContextId += 2; } @@ -456,36 +451,56 @@ } } + void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax, + DicomAssociationRole role) + { + std::list<DicomTransferSyntax> ts; + ts.push_back(DicomTransferSyntax_LittleEndianExplicit); // the most standard one first ! + ts.push_back(DicomTransferSyntax_LittleEndianImplicit); + ts.push_back(DicomTransferSyntax_BigEndianExplicit); // Retired but was historicaly proposed by Orthanc + ProposePresentationContext(abstractSyntax, ts, role); + } void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax) { - std::set<DicomTransferSyntax> ts; - ts.insert(DicomTransferSyntax_LittleEndianImplicit); - ts.insert(DicomTransferSyntax_LittleEndianExplicit); - ts.insert(DicomTransferSyntax_BigEndianExplicit); // Retired - ProposePresentationContext(abstractSyntax, ts); + ProposeGenericPresentationContext(abstractSyntax, DicomAssociationRole_Default); } void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax, DicomTransferSyntax transferSyntax) { - std::set<DicomTransferSyntax> ts; - ts.insert(transferSyntax); - ProposePresentationContext(abstractSyntax, ts); + ProposePresentationContext(abstractSyntax, transferSyntax, DicomAssociationRole_Default); } - + + void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax, + DicomTransferSyntax transferSyntax, + DicomAssociationRole role) + { + std::list<DicomTransferSyntax> ts; + ts.push_back(transferSyntax); + ProposePresentationContext(abstractSyntax, ts, role); + } + 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::list<DicomTransferSyntax>& transferSyntaxes) + { + ProposePresentationContext(abstractSyntax, transferSyntaxes, DicomAssociationRole_Default); + } + void DicomAssociation::ProposePresentationContext( const std::string& abstractSyntax, - const std::set<DicomTransferSyntax>& transferSyntaxes) + const std::list<DicomTransferSyntax>& transferSyntaxes, + DicomAssociationRole role) { if (transferSyntaxes.empty()) { @@ -507,6 +522,7 @@ ProposedPresentationContext context; context.abstractSyntax_ = abstractSyntax; context.transferSyntaxes_ = transferSyntaxes; + context.role_ = role; proposed_.push_back(context); } @@ -526,6 +542,35 @@ } } + bool DicomAssociation::GetAssociationParameters(std::string& remoteAet, + std::string& remoteIp, + std::string& calledAet) const + { + T_ASC_Association& dcmtkAssoc = GetDcmtkAssociation(); + + DIC_AE remoteAet_C; + DIC_AE calledAet_C; + DIC_AE remoteIp_C; + DIC_AE calledIP_C; + + if ( +#if DCMTK_VERSION_NUMBER >= 364 + ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).good() && + ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).good() +#else + ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, calledAet_C, NULL).good() && + ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, calledIP_C).good() +#endif + ) + { + remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C)); + remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C)); + calledAet = (/*OFSTRING_GUARD*/(calledAet_C)); + return true; + } + + return false; + } T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const { @@ -649,13 +694,12 @@ DicomAssociation association; { - std::set<DicomTransferSyntax> transferSyntaxes; - transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); - transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); + std::list<DicomTransferSyntax> transferSyntaxes; + transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit); + transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit); - association.SetRole(DicomAssociationRole_Scp); association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, - transferSyntaxes); + transferSyntaxes, DicomAssociationRole_Scp); } association.Open(parameters); @@ -828,13 +872,13 @@ DicomAssociation association; { - std::set<DicomTransferSyntax> transferSyntaxes; - transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); - transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); + std::list<DicomTransferSyntax> transferSyntaxes; + transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit); + transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit); - association.SetRole(DicomAssociationRole_Default); + // association.SetRole(DicomAssociationRole_Default); association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, - transferSyntaxes); + transferSyntaxes, DicomAssociationRole_Default); } association.Open(parameters);
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h Thu Jan 16 16:40:08 2025 +0100 @@ -44,6 +44,7 @@ #include <stdint.h> // For uint8_t #include <boost/noncopyable.hpp> #include <set> +#include <list> namespace Orthanc { @@ -58,13 +59,13 @@ struct ProposedPresentationContext { std::string abstractSyntax_; - std::set<DicomTransferSyntax> transferSyntaxes_; + std::list<DicomTransferSyntax> transferSyntaxes_; + DicomAssociationRole role_; }; typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> > AcceptedPresentationContexts; - DicomAssociationRole role_; bool isOpen_; std::vector<ProposedPresentationContext> proposed_; AcceptedPresentationContexts accepted_; @@ -95,8 +96,6 @@ return isOpen_; } - void SetRole(DicomAssociationRole role); - void ClearPresentationContexts(); void Open(const DicomAssociationParameters& parameters); @@ -109,6 +108,13 @@ void ProposeGenericPresentationContext(const std::string& abstractSyntax); + void ProposeGenericPresentationContext(const std::string& abstractSyntax, + DicomAssociationRole role); + + void ProposePresentationContext(const std::string& abstractSyntax, + DicomTransferSyntax transferSyntax, + DicomAssociationRole role); + void ProposePresentationContext(const std::string& abstractSyntax, DicomTransferSyntax transferSyntax); @@ -116,12 +122,21 @@ void ProposePresentationContext( const std::string& abstractSyntax, - const std::set<DicomTransferSyntax>& transferSyntaxes); - + const std::list<DicomTransferSyntax>& transferSyntaxes); + + void ProposePresentationContext( + const std::string& abstractSyntax, + const std::list<DicomTransferSyntax>& transferSyntaxes, + DicomAssociationRole role); + T_ASC_Association& GetDcmtkAssociation() const; T_ASC_Network& GetDcmtkNetwork() const; + bool GetAssociationParameters(std::string& remoteAet, + std::string& remoteIp, + std::string& calledAet) const; + static void CheckCondition(const OFCondition& cond, const DicomAssociationParameters& parameters, const std::string& command);
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -234,15 +234,58 @@ - void DicomControlUserConnection::SetupPresentationContexts() + void DicomControlUserConnection::SetupPresentationContexts( + ScuOperationFlags scuOperation, + const std::set<std::string>& acceptedStorageSopClasses, + const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes) { assert(association_.get() != NULL); - association_->ProposeGenericPresentationContext(UID_VerificationSOPClass); - association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel); - association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel); - association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel); - association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel); - association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel); + + if ((scuOperation & ScuOperationFlags_Echo) != 0) + { + association_->ProposeGenericPresentationContext(UID_VerificationSOPClass); + } + + if ((scuOperation & ScuOperationFlags_FindPatient) != 0) + { + association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel); + } + + if ((scuOperation & ScuOperationFlags_FindStudy) != 0) + { + association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel); + } + + if ((scuOperation & ScuOperationFlags_FindWorklist) != 0) + { + association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel); + } + + if ((scuOperation & ScuOperationFlags_MovePatient) != 0) + { + association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel); + } + + if ((scuOperation & ScuOperationFlags_MoveStudy) != 0) + { + association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel); + } + + if ((scuOperation & ScuOperationFlags_Get) != 0) + { + association_->ProposeGenericPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel); + association_->ProposeGenericPresentationContext(UID_GETPatientRootQueryRetrieveInformationModel); + + if (acceptedStorageSopClasses.size() == 0) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // the acceptedStorageSopClassUids should always be defined for a C-Get + } + + for (std::set<std::string>::const_iterator it = acceptedStorageSopClasses.begin(); it != acceptedStorageSopClasses.end(); ++it) + { + association_->ProposePresentationContext(*it, proposedStorageTransferSyntaxes, DicomAssociationRole_Scp); + } + } } @@ -350,6 +393,21 @@ } } + void MoveProgressCallback(void *callbackData, + T_DIMSE_C_MoveRQ *request, + int responseCount, + T_DIMSE_C_MoveRSP *response) + { + DicomControlUserConnection::IProgressListener* listener = reinterpret_cast<DicomControlUserConnection::IProgressListener*>(callbackData); + if (listener) + { + listener->OnProgressUpdated(response->NumberOfRemainingSubOperations, + response->NumberOfCompletedSubOperations, + response->NumberOfFailedSubOperations, + response->NumberOfWarningSubOperations); + } + } + void DicomControlUserConnection::MoveInternal(const std::string& targetAet, ResourceType level, @@ -391,7 +449,8 @@ DcmDataset* statusDetail = NULL; DcmDataset* responseIdentifiers = NULL; OFCondition cond = DIMSE_moveUser( - &association_->GetDcmtkAssociation(), presID, &request, dataset, /*moveCallback*/ NULL, NULL, + &association_->GetDcmtkAssociation(), presID, &request, dataset, + (progressListener_ != NULL ? MoveProgressCallback : NULL), progressListener_, /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), /*opt_dimse_timeout*/ parameters_.GetTimeout(), &association_->GetDcmtkNetwork(), /*subOpCallback*/ NULL, NULL, @@ -413,6 +472,14 @@ OFString str; CLOG(TRACE, DICOM) << "Received Final Move Response:" << std::endl << DIMSE_dumpMessage(str, response, DIMSE_INCOMING); + + if (progressListener_ != NULL) + { + progressListener_->OnProgressUpdated(response.NumberOfRemainingSubOperations, + response.NumberOfCompletedSubOperations, + response.NumberOfFailedSubOperations, + response.NumberOfWarningSubOperations); + } } /** @@ -445,11 +512,240 @@ } - DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) : + void DicomControlUserConnection::Get(const DicomMap& findResult, + CGetInstanceReceivedCallback instanceReceivedCallback, + void* callbackContext) + { + assert(association_.get() != NULL); + association_->Open(parameters_); + + std::unique_ptr<ParsedDicomFile> query( + ConvertQueryFields(findResult, parameters_.GetRemoteModality().GetManufacturer())); + DcmDataset* queryDataset = query->GetDcmtkObject().getDataset(); + + std::string remoteAet; + std::string remoteIp; + std::string calledAet; + + association_->GetAssociationParameters(remoteAet, remoteIp, calledAet); + + const char* sopClass = NULL; + const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); + ResourceType level = StringToResourceType(tmp.c_str()); + switch (level) + { + case ResourceType_Patient: + sopClass = UID_GETPatientRootQueryRetrieveInformationModel; + break; + case ResourceType_Study: + case ResourceType_Series: + case ResourceType_Instance: + sopClass = UID_GETStudyRootQueryRetrieveInformationModel; + break; + default: + throw OrthancException(ErrorCode_InternalError); + } + + // Figure out which of the accepted presentation contexts should be used + int cgetPresID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass); + if (cgetPresID == 0) + { + throw OrthancException(ErrorCode_DicomGetUnavailable, + "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle()); + } + + T_DIMSE_Message msgGetRequest; + memset((char*)&msgGetRequest, 0, sizeof(msgGetRequest)); + msgGetRequest.CommandField = DIMSE_C_GET_RQ; + + T_DIMSE_C_GetRQ* request = &(msgGetRequest.msg.CGetRQ); + request->MessageID = association_->GetDcmtkAssociation().nextMsgID++; + strncpy(request->AffectedSOPClassUID, sopClass, DIC_UI_LEN); + request->Priority = DIMSE_PRIORITY_MEDIUM; + request->DataSetType = DIMSE_DATASET_PRESENT; + + { + OFString str; + CLOG(TRACE, DICOM) << "Sending Get Request:" << std::endl + << DIMSE_dumpMessage(str, *request, DIMSE_OUTGOING, NULL, cgetPresID); + } + + OFCondition cond = DIMSE_sendMessageUsingMemoryData( + &(association_->GetDcmtkAssociation()), cgetPresID, &msgGetRequest, NULL /* statusDetail */, queryDataset, + NULL, NULL, NULL /* commandSet */); + + if (cond.bad()) + { + OFString tempStr; + CLOG(TRACE, DICOM) << "Failed sending C-GET request: " << DimseCondition::dump(tempStr, cond); + // return cond; + } + + // equivalent to handleCGETSession in DCMTK + bool continueSession = true; + + // As long we want to continue (usually, as long as we receive more objects, + // i.e. the final C-GET response has not arrived yet) + while (continueSession) + { + T_DIMSE_Message rsp; + // Make sure everything is zeroed (especially options) + memset((char*)&rsp, 0, sizeof(rsp)); + + // DcmDataset* statusDetail = NULL; + T_ASC_PresentationContextID cmdPresId = 0; + + OFCondition result = DIMSE_receiveCommand(&(association_->GetDcmtkAssociation()), + (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + parameters_.GetTimeout(), + &cmdPresId, + &rsp, + NULL /* statusDetail */, + NULL /* not interested in the command set */); + + if (result.bad()) + { + OFString tempStr; + CLOG(TRACE, DICOM) << "Failed receiving DIMSE command: " << DimseCondition::dump(tempStr, result); + // delete statusDetail; + break; // TODO: return value + } + // Handle C-GET Response + if (rsp.CommandField == DIMSE_C_GET_RSP) + { + { + OFString tempStr; + CLOG(TRACE, DICOM) << "Received C-GET Response: " << std::endl + << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId); + } + + if (progressListener_ != NULL) + { + progressListener_->OnProgressUpdated(rsp.msg.CGetRSP.NumberOfRemainingSubOperations, + rsp.msg.CGetRSP.NumberOfCompletedSubOperations, + rsp.msg.CGetRSP.NumberOfFailedSubOperations, + rsp.msg.CGetRSP.NumberOfWarningSubOperations); + } + + if (rsp.msg.CGetRSP.DimseStatus == 0x0000) // final success message + { + continueSession = false; + } + } + // Handle C-STORE Request + else if (rsp.CommandField == DIMSE_C_STORE_RQ) + { + { + OFString tempStr; + CLOG(TRACE, DICOM) << "Received C-STORE Request: " << std::endl + << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId); + } + + T_DIMSE_C_StoreRQ* storeRequest = &(rsp.msg.CStoreRQ); + + // Check if dataset is announced correctly + if (rsp.msg.CStoreRQ.DataSetType == DIMSE_DATASET_NULL) + { + CLOG(WARNING, DICOM) << "C-GET SCU handler: Incoming C-STORE with no dataset"; + } + + Uint16 desiredCStoreReturnStatus = 0; + DcmDataset* dataObject = NULL; + + // Receive dataset + result = DIMSE_receiveDataSetInMemory(&(association_->GetDcmtkAssociation()), + (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + parameters_.GetTimeout(), + &cmdPresId, + &dataObject, + NULL, NULL); + + if (result.bad()) + { + LOG(WARNING) << "C-GET SCU handler: Failed to receive dataset: " << result.text(); + desiredCStoreReturnStatus = STATUS_STORE_Error_CannotUnderstand; + } + else + { + // callback the OrthancServer with the received data + if (instanceReceivedCallback != NULL) + { + desiredCStoreReturnStatus = instanceReceivedCallback(callbackContext, *dataObject, remoteAet, remoteIp, calledAet); + } + + // send the Store response + T_DIMSE_Message storeResponse; + memset((char*)&storeResponse, 0, sizeof(storeResponse)); + storeResponse.CommandField = DIMSE_C_STORE_RSP; + + T_DIMSE_C_StoreRSP& storeRsp = storeResponse.msg.CStoreRSP; + storeRsp.MessageIDBeingRespondedTo = storeRequest->MessageID; + storeRsp.DimseStatus = desiredCStoreReturnStatus; + storeRsp.DataSetType = DIMSE_DATASET_NULL; + + OFStandard::strlcpy( + storeRsp.AffectedSOPClassUID, storeRequest->AffectedSOPClassUID, sizeof(storeRsp.AffectedSOPClassUID)); + OFStandard::strlcpy( + storeRsp.AffectedSOPInstanceUID, storeRequest->AffectedSOPInstanceUID, sizeof(storeRsp.AffectedSOPInstanceUID)); + storeRsp.opts = O_STORE_AFFECTEDSOPCLASSUID | O_STORE_AFFECTEDSOPINSTANCEUID; + + result = DIMSE_sendMessageUsingMemoryData(&(association_->GetDcmtkAssociation()), + cmdPresId, + &storeResponse, NULL /* statusDetail */, NULL /* dataObject */, + NULL, NULL, NULL /* commandSet */); + if (result.bad()) + { + continueSession = false; + } + else + { + OFString tempStr; + CLOG(TRACE, DICOM) << "Sent C-STORE Response: " << std::endl + << DIMSE_dumpMessage(tempStr, storeResponse, DIMSE_OUTGOING, NULL, cmdPresId); + } + } + } + // Handle other DIMSE command (error since other command than GET/STORE not expected) + else + { + CLOG(WARNING, DICOM) << "Expected C-GET response or C-STORE request but received DIMSE command 0x" + << std::hex << std::setfill('0') << std::setw(4) + << static_cast<unsigned int>(rsp.CommandField); + + result = DIMSE_BADCOMMANDTYPE; + continueSession = false; + } + + // delete statusDetail; // should be NULL if not existing or added to response list + // statusDetail = NULL; + } + /* All responses received or break signal occurred */ + + // return result; +} + + + DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation) : parameters_(params), - association_(new DicomAssociation) + association_(new DicomAssociation), + progressListener_(NULL) { - SetupPresentationContexts(); + assert((scuOperation & ScuOperationFlags_Get) == 0); // you must provide acceptedStorageSopClassUids for Get SCU + std::set<std::string> emptyStorageSopClasses; + std::list<DicomTransferSyntax> emptyStorageTransferSyntaxes; + + SetupPresentationContexts(scuOperation, emptyStorageSopClasses, emptyStorageTransferSyntaxes); + } + + DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, + ScuOperationFlags scuOperation, + const std::set<std::string>& acceptedStorageSopClasses, + const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes) : + parameters_(params), + association_(new DicomAssociation), + progressListener_(NULL) + { + SetupPresentationContexts(scuOperation, acceptedStorageSopClasses, proposedStorageTransferSyntaxes); }
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h Thu Jan 16 16:40:08 2025 +0100 @@ -37,13 +37,50 @@ { class DicomAssociation; // Forward declaration for PImpl design pattern + typedef uint16_t (*CGetInstanceReceivedCallback)(void *callbackContext, + DcmDataset& dataset, + const std::string& remoteAet, + const std::string& remoteIp, + const std::string& calledAet + ); + + + enum ScuOperationFlags + { + ScuOperationFlags_Echo = 1 << 0, + ScuOperationFlags_FindPatient = 1 << 1, + ScuOperationFlags_FindStudy = 1 << 2, + ScuOperationFlags_FindWorklist = 1 << 3, + ScuOperationFlags_MoveStudy = 1 << 4, + ScuOperationFlags_MovePatient = 1 << 5, + // C-Store is not using DicomControlUserConnection but DicomStoreUserConnection + ScuOperationFlags_Get = 1 << 6, + + ScuOperationFlags_Find = ScuOperationFlags_FindPatient | ScuOperationFlags_FindStudy | ScuOperationFlags_FindWorklist, + ScuOperationFlags_Move = ScuOperationFlags_MoveStudy | ScuOperationFlags_MovePatient, + ScuOperationFlags_All = ScuOperationFlags_Echo | ScuOperationFlags_Find | ScuOperationFlags_Move | ScuOperationFlags_Get + }; + class DicomControlUserConnection : public boost::noncopyable { + public: + class IProgressListener + { + public: + virtual void OnProgressUpdated(uint16_t nbRemainingSubOperations, + uint16_t nbCompletedSubOperations, + uint16_t nbFailedSubOperations, + uint16_t nbWarningSubOperations) = 0; + }; + private: DicomAssociationParameters parameters_; boost::shared_ptr<DicomAssociation> association_; + IProgressListener* progressListener_; - void SetupPresentationContexts(); + void SetupPresentationContexts(ScuOperationFlags scuOperation, + const std::set<std::string>& acceptedStorageSopClasses, + const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes); void FindInternal(DicomFindAnswers& answers, DcmDataset* dataset, @@ -56,8 +93,14 @@ const DicomMap& fields); public: - explicit DicomControlUserConnection(const DicomAssociationParameters& params); - + explicit DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation); + + // specific constructor for CGet SCU + explicit DicomControlUserConnection(const DicomAssociationParameters& params, + ScuOperationFlags scuOperation, + const std::set<std::string>& acceptedStorageSopClasses, + const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes); + const DicomAssociationParameters& GetParameters() const { return parameters_; @@ -67,11 +110,20 @@ bool Echo(); + void SetProgressListener(IProgressListener* progressListener) + { + progressListener_ = progressListener; + } + void Find(DicomFindAnswers& result, ResourceType level, const DicomMap& originalFields, bool normalize); + void Get(const DicomMap& getQuery, + CGetInstanceReceivedCallback instanceReceivedCallback, + void* callbackContext); + void Move(const std::string& targetAet, ResourceType level, const DicomMap& findResult);
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -57,7 +57,7 @@ bool hasPreferred, DicomTransferSyntax preferred) { - typedef std::list< std::set<DicomTransferSyntax> > GroupsOfSyntaxes; + typedef std::list< std::list<DicomTransferSyntax> > GroupsOfSyntaxes; GroupsOfSyntaxes groups; @@ -65,8 +65,8 @@ for (std::set<DicomTransferSyntax>::const_iterator it = sourceSyntaxes.begin(); it != sourceSyntaxes.end(); ++it) { - std::set<DicomTransferSyntax> group; - group.insert(*it); + std::list<DicomTransferSyntax> group; + group.push_back(*it); groups.push_back(group); } @@ -74,8 +74,8 @@ if (hasPreferred && sourceSyntaxes.find(preferred) == sourceSyntaxes.end()) { - std::set<DicomTransferSyntax> group; - group.insert(preferred); + std::list<DicomTransferSyntax> group; + group.push_back(preferred); groups.push_back(group); } @@ -89,7 +89,7 @@ DicomTransferSyntax_BigEndianExplicit }; - std::set<DicomTransferSyntax> group; + std::list<DicomTransferSyntax> group; for (size_t i = 0; i < N; i++) { @@ -97,7 +97,7 @@ if (sourceSyntaxes.find(syntax) == sourceSyntaxes.end() && (!hasPreferred || preferred != syntax)) { - group.insert(syntax); + group.push_back(syntax); } }
--- a/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h Thu Jan 16 16:40:08 2025 +0100 @@ -28,6 +28,7 @@ #include <boost/noncopyable.hpp> #include <string> +#include <list> namespace Orthanc { @@ -47,13 +48,23 @@ const std::string& calledAet, DicomRequestType type) = 0; + // Get the set of TransferSyntaxes that are accepted when negotiation a C-Store association, acting as SCP when it has been initiated by the C-Store SCU. virtual void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& target, const std::string& remoteIp, const std::string& remoteAet, const std::string& calledAet) = 0; - + + // Get the list of TransferSyntaxes that are proposed when initiating a C-Store SCP which actually only happens in a C-Get SCU + virtual void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& target, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; + virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp, const std::string& remoteAet, const std::string& calledAet) = 0; + + virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses, + size_t maxCount) = 0; }; }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -502,13 +502,21 @@ } else // see dcmqrsrv.cc lines 839 - 876 { + std::set<std::string> acceptedStorageClasses; + + if (server.HasApplicationEntityFilter()) + { + server.GetApplicationEntityFilter().GetAcceptedSopClasses(acceptedStorageClasses, 0); + } + /* accept storage syntaxes with proposed role */ int npc = ASC_countPresentationContexts(assoc->params); for (int i = 0; i < npc; i++) { T_ASC_PresentationContext pc; ASC_getPresentationContext(assoc->params, i, &pc); - if (dcmIsaStorageSOPClassUID(pc.abstractSyntax)) + if (acceptedStorageClasses.find(pc.abstractSyntax) != acceptedStorageClasses.end() + || (!server.HasApplicationEntityFilter() && dcmIsaStorageSOPClassUID(pc.abstractSyntax))) // previous behavior kept for compatibility in case the server does not have an ApplicationEntityFilter { /** * We are prepared to accept whatever role the caller
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -50,6 +50,7 @@ static const char* KEY_USE_DICOM_TLS = "UseDicomTls"; static const char* KEY_LOCAL_AET = "LocalAet"; static const char* KEY_TIMEOUT = "Timeout"; +static const char* KEY_RETRIEVE_METHOD = "RetrieveMethod"; namespace Orthanc @@ -72,6 +73,7 @@ useDicomTls_ = false; localAet_.clear(); timeout_ = 0; + retrieveMethod_ = RetrieveMethod_SystemDefault; } @@ -308,6 +310,17 @@ { timeout_ = SerializationToolbox::ReadUnsignedInteger(serialized, KEY_TIMEOUT); } + + if (serialized.isMember(KEY_RETRIEVE_METHOD)) + { + retrieveMethod_ = StringToRetrieveMethod + (SerializationToolbox::ReadString(serialized, KEY_RETRIEVE_METHOD)); + } + else + { + retrieveMethod_ = RetrieveMethod_SystemDefault; + } + } @@ -427,6 +440,7 @@ target[KEY_USE_DICOM_TLS] = useDicomTls_; target[KEY_LOCAL_AET] = localAet_; target[KEY_TIMEOUT] = timeout_; + target[KEY_RETRIEVE_METHOD] = EnumerationToString(retrieveMethod_); } else { @@ -521,4 +535,14 @@ { return timeout_ != 0; } + + RetrieveMethod RemoteModalityParameters::GetRetrieveMethod() const + { + return retrieveMethod_; + } + + void RemoteModalityParameters::SetRetrieveMethod(RetrieveMethod retrieveMethod) + { + retrieveMethod_ = retrieveMethod; + } }
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h Thu Jan 16 16:40:08 2025 +0100 @@ -51,6 +51,7 @@ bool useDicomTls_; std::string localAet_; uint32_t timeout_; + RetrieveMethod retrieveMethod_; // New in Orthanc 1.12.6 void Clear(); @@ -118,5 +119,10 @@ uint32_t GetTimeout() const; bool HasTimeout() const; + + RetrieveMethod GetRetrieveMethod() const; + + void SetRetrieveMethod(RetrieveMethod retrieveMethod); + }; }
--- a/OrthancFramework/Sources/Enumerations.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/Enumerations.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -372,6 +372,9 @@ case ErrorCode_NoCGetHandler: return "No request handler factory for DICOM C-GET SCP"; + case ErrorCode_DicomGetUnavailable: + return "DicomUserConnection: The C-GET command is not supported by the remote SCP"; + case ErrorCode_UnsupportedMediaType: return "Unsupported media type"; @@ -2493,6 +2496,44 @@ } } + RetrieveMethod StringToRetrieveMethod(const std::string& str) + { + if (str == "C-MOVE") + { + return RetrieveMethod_Move; + } + else if (str == "C-GET") + { + return RetrieveMethod_Get; + } + else if (str == "SystemDefault") + { + return RetrieveMethod_SystemDefault; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "RetrieveMethod can be \"C-MOVE\", \"C-GET\" or \"SystemDefault\": " + str); + } + } + + const char* EnumerationToString(RetrieveMethod method) + { + switch (method) + { + case RetrieveMethod_Get: + return "C-GET"; + + case RetrieveMethod_Move: + return "C-MOVE"; + + case RetrieveMethod_SystemDefault: + return "SystemDefault"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } }
--- a/OrthancFramework/Sources/Enumerations.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/Enumerations.h Thu Jan 16 16:40:08 2025 +0100 @@ -234,6 +234,7 @@ ErrorCode_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, ErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, ErrorCode_NoCGetHandler = 2044 /*!< No request handler factory for DICOM C-GET SCP */, + ErrorCode_DicomGetUnavailable = 2045 /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */, ErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, ErrorCode_START_PLUGINS = 1000000 }; @@ -793,6 +794,14 @@ ResourceType_Instance = 4 }; + enum RetrieveMethod // new in Orthanc 1.12.6 + { + RetrieveMethod_Move = 1, + RetrieveMethod_Get = 2, + + RetrieveMethod_SystemDefault = 65535 + }; + ORTHANC_PUBLIC const char* EnumerationToString(ErrorCode code); @@ -849,6 +858,9 @@ const char* EnumerationToString(DicomToJsonFormat format); ORTHANC_PUBLIC + const char* EnumerationToString(RetrieveMethod method); + + ORTHANC_PUBLIC Encoding StringToEncoding(const char* encoding); ORTHANC_PUBLIC @@ -947,4 +959,7 @@ ORTHANC_PUBLIC void GetAllDicomTransferSyntaxes(std::set<DicomTransferSyntax>& target); + + ORTHANC_PUBLIC + RetrieveMethod StringToRetrieveMethod(const std::string& str); }
--- a/OrthancFramework/Sources/JobsEngine/IJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/IJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -49,13 +49,18 @@ // For pausing/canceling/ending jobs: This method must release allocated resources virtual void Stop(JobStopReason reason) = 0; - virtual float GetProgress() = 0; + virtual float GetProgress() const = 0; - virtual void GetJobType(std::string& target) = 0; + virtual bool NeedsProgressUpdateBetweenSteps() const // only for jobs whose progress is updated by outside events (like C-Move and C-Get) + { + return false; + } + + virtual void GetJobType(std::string& target) const = 0; - virtual void GetPublicContent(Json::Value& value) = 0; + virtual void GetPublicContent(Json::Value& value) const = 0; - virtual bool Serialize(Json::Value& value) = 0; + virtual bool Serialize(Json::Value& value) const = 0; // This function can only be called if the job has reached its // "success" state
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -54,7 +54,8 @@ const JobStatus& status, const boost::posix_time::ptime& creationTime, const boost::posix_time::ptime& lastStateChangeTime, - const boost::posix_time::time_duration& runtime) : + const boost::posix_time::time_duration& runtime, + const IJob& job) : id_(id), priority_(priority), state_(state), @@ -68,11 +69,16 @@ if (state_ == JobState_Running) { float ms = static_cast<float>(runtime_.total_milliseconds()); + if (job.NeedsProgressUpdateBetweenSteps()) + { + status_.UpdateProgress(job); + } - if (status_.GetProgress() > 0.01f && + float progress = status_.GetProgress(); + + if (progress > 0.01f && ms > 0.01f) { - float progress = status_.GetProgress(); long long remaining = boost::math::llround(ms / progress * (1.0f - progress)); eta_ = timestamp_ + boost::posix_time::milliseconds(remaining); hasEta_ = true;
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/JobInfo.h Thu Jan 16 16:40:08 2025 +0100 @@ -51,7 +51,8 @@ const JobStatus& status, const boost::posix_time::ptime& creationTime, const boost::posix_time::ptime& lastStateChangeTime, - const boost::posix_time::time_duration& runtime) ORTHANC_LOCAL; + const boost::posix_time::time_duration& runtime, + const IJob& job) ORTHANC_LOCAL; JobInfo();
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -41,7 +41,7 @@ JobStatus::JobStatus(ErrorCode code, const std::string& details, - IJob& job) : + const IJob& job) : errorCode_(code), progress_(job.GetProgress()), publicContent_(Json::objectValue),
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/JobStatus.h Thu Jan 16 16:40:08 2025 +0100 @@ -44,7 +44,7 @@ JobStatus(ErrorCode code, const std::string& details, - IJob& job); + const IJob& job); ErrorCode GetErrorCode() const { @@ -61,6 +61,11 @@ return progress_; } + void UpdateProgress(const IJob& job) + { + progress_ = job.GetProgress(); + } + const std::string& GetJobType() const { return jobType_;
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -132,7 +132,10 @@ if (running.IsValid()) { - CLOG(INFO, JOBS) << "Executing job with priority " << running.GetPriority() + std::string jobType; + running.GetJob().GetJobType(jobType); + + CLOG(INFO, JOBS) << "Executing " << jobType << " job with priority " << running.GetPriority() << " in worker thread " << workerIndex << ": " << running.GetId(); while (engine->IsRunning())
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -223,8 +223,14 @@ lastStateChangeTime_ = time; } - const boost::posix_time::time_duration& GetRuntime() const + boost::posix_time::time_duration GetRuntime() const { + if (state_ == JobState_Running) + { + const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + return now - lastStateChangeTime_; + } + return runtime_; } @@ -644,7 +650,8 @@ handler.GetLastStatus(), handler.GetCreationTime(), handler.GetLastStateChangeTime(), - handler.GetRuntime()); + handler.GetRuntime(), + handler.GetJob()); return true; } } @@ -792,7 +799,10 @@ } } - LOG(INFO) << "New job submitted with priority " << priority << ": " << id; + std::string jobType; + handler->GetJob().GetJobType(jobType); + + LOG(INFO) << "New " << jobType << " job submitted with priority " << priority << ": " << id; if (observer_ != NULL) {
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -398,7 +398,7 @@ } - float SequenceOfOperationsJob::GetProgress() + float SequenceOfOperationsJob::GetProgress() const { boost::mutex::scoped_lock lock(mutex_); @@ -406,13 +406,13 @@ static_cast<float>(operations_.size() + 1)); } - void SequenceOfOperationsJob::GetJobType(std::string& target) + void SequenceOfOperationsJob::GetJobType(std::string& target) const { target = "SequenceOfOperations"; } - void SequenceOfOperationsJob::GetPublicContent(Json::Value& value) + void SequenceOfOperationsJob::GetPublicContent(Json::Value& value) const { boost::mutex::scoped_lock lock(mutex_); @@ -421,7 +421,7 @@ } - bool SequenceOfOperationsJob::Serialize(Json::Value& value) + bool SequenceOfOperationsJob::Serialize(Json::Value& value) const { boost::mutex::scoped_lock lock(mutex_);
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -54,7 +54,7 @@ std::string description_; bool done_; - boost::mutex mutex_; + mutable boost::mutex mutex_; std::vector<Operation*> operations_; size_t current_; boost::condition_variable operationAdded_; @@ -117,13 +117,13 @@ virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - virtual float GetProgress() ORTHANC_OVERRIDE; + virtual float GetProgress() const ORTHANC_OVERRIDE; - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE; + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE; - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE; virtual bool GetOutput(std::string& output, MimeType& mime,
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -137,7 +137,7 @@ } - float SetOfCommandsJob::GetProgress() + float SetOfCommandsJob::GetProgress() const { if (commands_.empty()) { @@ -237,13 +237,13 @@ static const char* KEY_COMMANDS = "Commands"; - void SetOfCommandsJob::GetPublicContent(Json::Value& value) + void SetOfCommandsJob::GetPublicContent(Json::Value& value) const { value[KEY_DESCRIPTION] = GetDescription(); } - bool SetOfCommandsJob::Serialize(Json::Value& target) + bool SetOfCommandsJob::Serialize(Json::Value& target) const { target = Json::objectValue;
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -92,7 +92,7 @@ virtual void Start() ORTHANC_OVERRIDE; - virtual float GetProgress() ORTHANC_OVERRIDE; + virtual float GetProgress() const ORTHANC_OVERRIDE; bool IsStarted() const; @@ -100,9 +100,9 @@ virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; virtual bool GetOutput(std::string& output, MimeType& mime,
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -209,7 +209,7 @@ static const char* KEY_FAILED_INSTANCES = "FailedInstances"; static const char* KEY_PARENT_RESOURCES = "ParentResources"; - void SetOfInstancesJob::GetPublicContent(Json::Value& target) + void SetOfInstancesJob::GetPublicContent(Json::Value& target) const { SetOfCommandsJob::GetPublicContent(target); target["InstancesCount"] = static_cast<uint32_t>(GetInstancesCount()); @@ -222,7 +222,7 @@ } - bool SetOfInstancesJob::Serialize(Json::Value& target) + bool SetOfInstancesJob::Serialize(Json::Value& target) const { if (SetOfCommandsJob::Serialize(target)) {
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -77,8 +77,8 @@ virtual void Reset() ORTHANC_OVERRIDE; - virtual void GetPublicContent(Json::Value& target) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& target) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; }; }
--- a/OrthancFramework/Sources/Toolbox.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/Sources/Toolbox.h Thu Jan 16 16:40:08 2025 +0100 @@ -262,7 +262,7 @@ } } - // returns true if all element of 'needles' are found in 'haystack' + // returns the elements that are both in a and b template <typename T> static void GetIntersection(std::set<T>& target, const std::set<T>& a, const std::set<T>& b) { target.clear();
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -103,24 +103,24 @@ { } - virtual float GetProgress() ORTHANC_OVERRIDE + virtual float GetProgress() const ORTHANC_OVERRIDE { return static_cast<float>(count_) / static_cast<float>(steps_ - 1); } - virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE { type = "DummyJob"; } - virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE + virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE { value = Json::objectValue; value["Type"] = "DummyJob"; return true; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE { value["hello"] = "world"; } @@ -199,7 +199,7 @@ { } - virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& s) const ORTHANC_OVERRIDE { s = "DummyInstancesJob"; }
--- a/OrthancServer/CMakeLists.txt Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/CMakeLists.txt Thu Jan 16 16:40:08 2025 +0100 @@ -138,6 +138,8 @@ ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ArchiveJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/CleaningInstancesJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomModalityStoreJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomGetScuJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomMoveScuJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/LuaJobManager.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/MergeStudyJob.cpp @@ -184,6 +186,7 @@ ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp
--- a/OrthancServer/OrthancExplorer/query-retrieve.js Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/OrthancExplorer/query-retrieve.js Thu Jan 16 16:40:08 2025 +0100 @@ -341,9 +341,9 @@ success: function(system) { $('#retrieve-target').val(system['DicomAet']); + $('#retrieve-form').unbind('submit'); $('#retrieve-form').submit(function(event) { var aet; - event.preventDefault(); aet = $('#retrieve-target').val();
--- a/OrthancServer/Plugins/Engine/PluginsJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Plugins/Engine/PluginsJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -147,7 +147,7 @@ } } - float PluginsJob::GetProgress() + float PluginsJob::GetProgress() const { return parameters_.getProgress(parameters_.job); } @@ -194,7 +194,7 @@ }; } - void PluginsJob::GetPublicContent(Json::Value& value) + void PluginsJob::GetPublicContent(Json::Value& value) const { if (parameters_.getContent != NULL) { @@ -232,7 +232,7 @@ } } - bool PluginsJob::Serialize(Json::Value& value) + bool PluginsJob::Serialize(Json::Value& value) const { if (parameters_.getSerialized != NULL) {
--- a/OrthancServer/Plugins/Engine/PluginsJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Plugins/Engine/PluginsJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -58,16 +58,16 @@ virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - virtual float GetProgress() ORTHANC_OVERRIDE; + virtual float GetProgress() const ORTHANC_OVERRIDE; - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE { target = type_; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE; virtual bool GetOutput(std::string& output, MimeType& mime,
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Thu Jan 16 16:40:08 2025 +0100 @@ -324,6 +324,7 @@ OrthancPluginErrorCode_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, OrthancPluginErrorCode_NoCGetHandler = 2044 /*!< No request handler factory for DICOM C-GET SCP */, + OrthancPluginErrorCode_DicomGetUnavailable = 2045 /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */, OrthancPluginErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -29,10 +29,11 @@ #include "../../../../OrthancFramework/Sources/OrthancException.h" #include "../Common/OrthancPluginCppWrapper.h" - +#include <dcmtk/dcmdata/dcuid.h> /* for variable dcmAllStorageSOPClassUIDs */ DicomFilter::DicomFilter() : - hasAcceptedTransferSyntaxes_(false) + hasAcceptedTransferSyntaxes_(false), + hasAcceptedStorageClasses_(false) { { OrthancPlugins::OrthancConfiguration config; @@ -200,6 +201,18 @@ } +void DicomFilter::GetProposedStorageTransferSyntaxes(std::list<Orthanc::DicomTransferSyntax>& target, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) +{ + // default TS + target.push_back(Orthanc::DicomTransferSyntax_LittleEndianExplicit); + target.push_back(Orthanc::DicomTransferSyntax_LittleEndianImplicit); + target.push_back(Orthanc::DicomTransferSyntax_BigEndianExplicit); +} + + bool DicomFilter::IsUnknownSopClassAccepted(const std::string& remoteIp, const std::string& remoteAet, const std::string& calledAet) @@ -207,3 +220,47 @@ boost::shared_lock<boost::shared_mutex> lock(mutex_); return unknownSopClassAccepted_; } + + +void DicomFilter::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) +{ + boost::unique_lock<boost::shared_mutex> lock(mutex_); + + if (!hasAcceptedStorageClasses_) + { + Json::Value jsonSopClasses; + + if (!OrthancPlugins::RestApiGet(jsonSopClasses, "/tools/accepted-sop-classes", false) || + jsonSopClasses.type() != Json::arrayValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + for (Json::Value::ArrayIndex i = 0; i < jsonSopClasses.size(); i++) + { + if (jsonSopClasses[i].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + acceptedStorageClasses_.insert(jsonSopClasses[i].asString()); + } + } + } + + hasAcceptedStorageClasses_ = true; + } + + std::set<std::string>::const_iterator it = acceptedStorageClasses_.begin(); + size_t count = 0; + + while (it != acceptedStorageClasses_.end() && (maxCount == 0 || count < maxCount)) + { + sopClasses.insert(*it); + count++; + it++; + } + +} \ No newline at end of file
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h Thu Jan 16 16:40:08 2025 +0100 @@ -44,6 +44,8 @@ bool hasAcceptedTransferSyntaxes_; std::set<Orthanc::DicomTransferSyntax> acceptedTransferSyntaxes_; + bool hasAcceptedStorageClasses_; + std::set<std::string> acceptedStorageClasses_; public: DicomFilter(); @@ -62,7 +64,15 @@ const std::string& remoteAet, const std::string& calledAet) ORTHANC_OVERRIDE; + virtual void GetProposedStorageTransferSyntaxes(std::list<Orthanc::DicomTransferSyntax>& target, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) ORTHANC_OVERRIDE; + + virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp, const std::string& remoteAet, const std::string& calledAet) ORTHANC_OVERRIDE; + + virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) ORTHANC_OVERRIDE; };
--- a/OrthancServer/Resources/Configuration.json Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Resources/Configuration.json Thu Jan 16 16:40:08 2025 +0100 @@ -202,6 +202,32 @@ // SOP classes (aka. "promiscuous mode") "UnknownSopClassAccepted" : false, + // The list of accepted Storage SOP classes. + // If empty or not defined, this list defaults + // to all storage classes defined in DCMTK in case of + // C-STORE SCP and to a reduced list of 120 common storage + // classes in case of C-GET SCU. + // Each entry can contain wildcards ("?" or "*") to add + // subsets of SOP classes that are defined in DCMTK. + // If you want to add a a SOP class that is not defined in + // DCMTK, you must add it explicitely. + // (new in Orthanc 1.12.6) + // "AcceptedSopClasses" : [ + // "1.2.840.10008.5.1.4.1.1.2", + // "1.2.840.10008.5.1.4.1.1.4" + // ] + + // The list of rejected Storage SOP classes. + // This configuration is only meaningful if + // "AcceptedSopClasses" is using regular expressions + // or if it is not defined. + // Each entry can contain wildcards ("?" or "*"). + // (new in Orthanc 1.12.6) + // "RejectedSopClasses" : [ + // "1.2.840.10008.5.1.4.1.1.2", + // "1.2.840.10008.5.1.4.1.1.4" + // ] + // Set the timeout (in seconds) after which the DICOM associations // are closed by the Orthanc SCP (server) if no further DIMSE // command is received from the SCU (client). @@ -459,6 +485,10 @@ * for Orthanc when initiating an SCU to this very specific * modality. Similarly, "Timeout" allows one to overwrite the * global value "DicomScuTimeout" on a per-modality basis. + * + * The "RetrieveMethod" option allows one to overwrite the global + * "DicomDefaultRetrieveMethod" configuration option for this + * specific modality. (Allowed values: "C-MOVE" or "C-GET"). **/ //"untrusted" : { // "AET" : "ORTHANC", @@ -475,7 +505,8 @@ // "AllowTranscoding" : true, // new in 1.7.0 // "UseDicomTls" : false, // new in 1.9.0 // "LocalAet" : "HELLO", // new in 1.9.0 - // "Timeout" : 60 // new in 1.9.1 + // "Timeout" : 60, // new in 1.9.1 + // "RetrieveMethod": "C-MOVE" // new in 1.12.6 //} }, @@ -489,6 +520,13 @@ // accept C-FIND requests from Orthanc (new in Orthanc 1.8.1). "DicomEchoChecksFind" : false, + // Wheter Orthanc uses C-MOVE or C-GET to retrieve a resource after + // a C-Find (when calling /queries/.../retrieve). + // This configuration can be overriden for each modality by providing + // "RetrieveMethod" in the "DicomModalities" entry. + // (new in Orthanc 1.12.6) + "DicomDefaultRetrieveMethod" : "C-MOVE", + // The timeout (in seconds) after which the DICOM associations are // considered as closed by the Orthanc SCU (client) if the remote // DICOM SCP (server) does not answer.
--- a/OrthancServer/Resources/DicomConformanceStatement.txt Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Resources/DicomConformanceStatement.txt Thu Jan 16 16:40:08 2025 +0100 @@ -209,6 +209,16 @@ MOVEStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.2 +------------------- +Get SCU Conformance +------------------- + +Orthanc supports the following SOP Classes as an SCU for C-Get: + + GETPatientRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.1.3 + GETStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.3 + + ----------------- Transfer Syntaxes -----------------
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -165,6 +165,7 @@ * 2. Select the preferred transfer syntaxes, which corresponds to * the source transfer syntax, plus all the uncompressed transfer * syntaxes if transcoding is enabled. + * This way, we minimize the transcoding on our side. **/ std::list<DicomTransferSyntax> preferred; @@ -208,7 +209,16 @@ } } - // No preferred syntax was accepted + // No preferred syntax was accepted but, if a PC has been accepted, it means that we have accepted a TS. + // This maybe means that we need to transcode twice on our side (from a compressed format to another compressed format). + if (allowTranscoding && accepted.size() > 0) + { + Accepted::const_iterator it = accepted.begin(); + selectedPresentationId = it->second; + selectedSyntax = it->first; + return true; + } + return false; }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -36,6 +36,7 @@ #include "../ServerContext.h" #include "../ServerJobs/DicomModalityStoreJob.h" #include "../ServerJobs/DicomMoveScuJob.h" +#include "../ServerJobs/DicomGetScuJob.h" #include "../ServerJobs/OrthancPeerStoreJob.h" #include "../ServerToolbox.h" #include "../StorageCommitmentReports.h" @@ -56,6 +57,7 @@ static const char* const KEY_CHECK_FIND = "CheckFind"; static const char* const SOP_CLASS_UID = "SOPClassUID"; static const char* const SOP_INSTANCE_UID = "SOPInstanceUID"; + static const char* const KEY_RETRIEVE_METHOD = "RetrieveMethod"; static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name) { @@ -155,24 +157,31 @@ const DicomAssociationParameters& parameters, const Json::Value& body) { - DicomControlUserConnection connection(parameters); - + bool checkFind = false; + + if (body.type() == Json::objectValue && + body.isMember(KEY_CHECK_FIND)) + { + checkFind = SerializationToolbox::ReadBoolean(body, KEY_CHECK_FIND); + } + else + { + OrthancConfiguration::ReaderLock lock; + checkFind = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false); + } + + ScuOperationFlags operations = ScuOperationFlags_Echo; + + if (checkFind) + { + operations = static_cast<ScuOperationFlags>(operations | ScuOperationFlags_Find); + } + + DicomControlUserConnection connection(parameters, operations); if (connection.Echo()) { - bool find = false; - - if (body.type() == Json::objectValue && - body.isMember(KEY_CHECK_FIND)) - { - find = SerializationToolbox::ReadBoolean(body, KEY_CHECK_FIND); - } - else - { - OrthancConfiguration::ReaderLock lock; - find = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false); - } - - if (find) + + if (checkFind) { // Issue a C-FIND request at the study level about a random Study Instance UID const std::string studyInstanceUid = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study); @@ -385,7 +394,7 @@ DicomFindAnswers answers(false); { - DicomControlUserConnection connection(GetAssociationParameters(call)); + DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindPatient); FindPatient(answers, connection, fields); } @@ -428,7 +437,7 @@ DicomFindAnswers answers(false); { - DicomControlUserConnection connection(GetAssociationParameters(call)); + DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy); FindStudy(answers, connection, fields); } @@ -472,7 +481,7 @@ DicomFindAnswers answers(false); { - DicomControlUserConnection connection(GetAssociationParameters(call)); + DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy); FindSeries(answers, connection, fields); } @@ -517,7 +526,7 @@ DicomFindAnswers answers(false); { - DicomControlUserConnection connection(GetAssociationParameters(call)); + DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy); FindInstance(answers, connection, fields); } @@ -566,7 +575,7 @@ return; } - DicomControlUserConnection connection(GetAssociationParameters(call)); + DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_Find); DicomFindAnswers patients(false); FindPatient(patients, connection, m); @@ -914,12 +923,25 @@ std::string targetAet; int timeout = -1; - + + QueryAccessor query(call); + + RetrieveMethod retrieveMethod = query.GetHandler().GetRemoteModality().GetRetrieveMethod(); + Json::Value body; if (call.ParseJsonRequest(body)) { + OrthancConfiguration::ReaderLock lock; + targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle()); timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1); + + std::string strRetrieveMethod = SerializationToolbox::ReadString(body, KEY_RETRIEVE_METHOD, ""); + + if (!strRetrieveMethod.empty()) + { + retrieveMethod = StringToRetrieveMethod(strRetrieveMethod); + } } else { @@ -934,45 +956,67 @@ } } - std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context)); - job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short)); - + if (retrieveMethod == RetrieveMethod_SystemDefault) + { + retrieveMethod = context.GetDefaultDicomRetrieveMethod(); + } + + std::unique_ptr<DicomRetrieveScuBaseJob> job; + + + switch (retrieveMethod) { - QueryAccessor query(call); - job->SetTargetAet(targetAet); - job->SetLocalAet(query.GetHandler().GetLocalAet()); - job->SetRemoteModality(query.GetHandler().GetRemoteModality()); - - if (timeout >= 0) + case RetrieveMethod_Move: { - // New in Orthanc 1.7.0 - job->SetTimeout(static_cast<uint32_t>(timeout)); - } - else if (query.GetHandler().HasTimeout()) + job.reset(new DicomMoveScuJob(context)); + (dynamic_cast<DicomMoveScuJob*>(job.get()))->SetTargetAet(targetAet); + + LOG(WARNING) << "Driving C-Move SCU on remote modality " + << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle() + << " to target modality " << targetAet; + }; break; + case RetrieveMethod_Get: { - // New in Orthanc 1.9.1 - job->SetTimeout(query.GetHandler().GetTimeout()); - } - - LOG(WARNING) << "Driving C-Move SCU on remote modality " - << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle() - << " to target modality " << targetAet; - - if (allAnswers) + job.reset(new DicomGetScuJob(context)); + + LOG(WARNING) << "Driving C-Get SCU on remote modality " + << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle(); + }; break; + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short)); + + job->SetLocalAet(query.GetHandler().GetLocalAet()); + job->SetRemoteModality(query.GetHandler().GetRemoteModality()); + + if (timeout >= 0) + { + // New in Orthanc 1.7.0 + job->SetTimeout(static_cast<uint32_t>(timeout)); + } + else if (query.GetHandler().HasTimeout()) + { + // New in Orthanc 1.9.1 + job->SetTimeout(query.GetHandler().GetTimeout()); + } + + if (allAnswers) + { + for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++) { - for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++) - { - job->AddFindAnswer(query.GetHandler(), i); - } + job->AddFindAnswer(query.GetHandler(), i); } - else - { - job->AddFindAnswer(query.GetHandler(), index); - } + } + else + { + job->AddFindAnswer(query.GetHandler(), index); } OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, body); + } @@ -989,6 +1033,10 @@ "`DicomAet` configuration option.", false) .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number, "Timeout for the C-MOVE command, in seconds", false) + .SetRequestField(KEY_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String, + "Force usage of C-MOVE or C-GET to retrieve the resource. If note defined in the payload, " + "the retrieve method is defined in the DicomDefaultRetrieveMethod configuration or in " + "DicomModalities->..->RetrieveMethod", false) .AddRequestType(MimeType_PlainText, "AET of the target modality"); } @@ -999,8 +1047,8 @@ { DocumentRetrieveShared(call); call.GetDocumentation() - .SetSummary("Retrieve one answer") - .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve one answer associated with the " + .SetSummary("Retrieve one answer with a C-MOVE or a C-GET SCU") + .SetDescription("Start a C-MOVE or a C-GET SCU command as a job, in order to retrieve one answer associated with the " "query/retrieve operation whose identifiers are provided in the URL: " "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move") .SetUriArgument("index", "Index of the answer"); @@ -1012,13 +1060,15 @@ } + + static void RetrieveAllAnswers(RestApiPostCall& call) { if (call.IsDocumentation()) { DocumentRetrieveShared(call); call.GetDocumentation() - .SetSummary("Retrieve all answers") + .SetSummary("Retrieve all answers with C-MOVE SCU") .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve all the answers associated with the " "query/retrieve operation whose identifier is provided in the URL: " "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move"); @@ -1536,6 +1586,48 @@ call.GetOutput().AnswerJson(answer); } + void ParseMoveGetJob(DicomRetrieveScuBaseJob& job, Json::Value& request, RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + if (!call.ParseJsonRequest(request) || + request.type() != Json::objectValue || + !request.isMember(KEY_RESOURCES) || + !request.isMember(KEY_LEVEL) || + request[KEY_RESOURCES].type() != Json::arrayValue || + request[KEY_LEVEL].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " + + std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL)); + } + + ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); + + std::string localAet = Toolbox::GetJsonStringField + (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle()); + + const RemoteModalityParameters source = + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + job.SetQueryFormat(DicomToJsonFormat_Short); + job.SetLocalAet(localAet); + job.SetRemoteModality(source); + + if (request.isMember(KEY_TIMEOUT)) + { + job.SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT)); + } + + for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++) + { + DicomMap resource; + FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements"); + + resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false); + + job.AddQuery(resource); + } + } /*************************************************************************** * DICOM C-Move SCU @@ -1569,53 +1661,15 @@ } ServerContext& context = OrthancRestApi::GetContext(call); - Json::Value request; - if (!call.ParseJsonRequest(request) || - request.type() != Json::objectValue || - !request.isMember(KEY_RESOURCES) || - !request.isMember(KEY_LEVEL) || - request[KEY_RESOURCES].type() != Json::arrayValue || - request[KEY_LEVEL].type() != Json::stringValue) - { - throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " + - std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL)); - } - - ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); - - std::string localAet = Toolbox::GetJsonStringField - (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle()); + std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context)); + + ParseMoveGetJob(*job, request, call); + std::string targetAet = Toolbox::GetJsonStringField (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle()); - - const RemoteModalityParameters source = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - - std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context)); - - job->SetQueryFormat(DicomToJsonFormat_Short); - - // QueryAccessor query(call); job->SetTargetAet(targetAet); - job->SetLocalAet(localAet); - job->SetRemoteModality(source); - - if (request.isMember(KEY_TIMEOUT)) - { - job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT)); - } - - for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++) - { - DicomMap resource; - FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements"); - - resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false); - - job->AddQuery(resource); - } OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, request); @@ -1623,6 +1677,51 @@ } + /*************************************************************************** + * DICOM C-Get SCU + ***************************************************************************/ + + static void DicomGet(RestApiPostCall& call) + { + if (call.IsDocumentation()) + { + OrthancRestApi::DocumentSubmitCommandsJob(call); + call.GetDocumentation() + .SetTag("Networking") + .SetSummary("Trigger C-GET SCU") + .SetDescription("Start a C-GET SCU command as a job, in order to retrieve DICOM resources " + "from a remote DICOM modality whose identifier is provided in the URL: ") + // "https://orthanc.uclouvain.be/book/users/rest.html#performing-c-move") // TODO-GET + .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfObjects, + "List of queries identifying all the DICOM resources to be sent. " + "Usage of wildcards is prohibited and the query shall only contain DICOM ID tags. " + "Additionally, you may provide SOPClassesInStudy to limit the scope of the DICOM " + "negotiation to certain SOPClassUID or to present uncommon SOPClassUID during " + "the DICOM negotation. By default, " + "Orhanc will propose the most 120 common SOPClassUIDs.", true) + .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject, + "A query object identifying all the DICOM resources to be retrieved", true) + .SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String, + "Local AET that is used for this commands, defaults to `DicomAet` configuration option. " + "Ignored if `DicomModalities` already sets `LocalAet` for this modality.", false) + .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number, + "Timeout for the C-GET command, in seconds", false) + .SetUriArgument("id", "Identifier of the modality of interest"); + return; + } + + ServerContext& context = OrthancRestApi::GetContext(call); + Json::Value request; + + std::unique_ptr<DicomGetScuJob> job(new DicomGetScuJob(context)); + + ParseMoveGetJob(*job, request, call); + + OrthancRestApi::GetApi(call).SubmitCommandsJob + (call, job.release(), true /* synchronous by default */, request); + return; + } + /*************************************************************************** * Orthanc Peers => Store client @@ -2213,7 +2312,7 @@ DicomFindAnswers answers(true); { - DicomControlUserConnection connection(GetAssociationParameters(call, json)); + DicomControlUserConnection connection(GetAssociationParameters(call, json), ScuOperationFlags_FindWorklist); connection.FindWorklist(answers, *query); } @@ -2544,6 +2643,7 @@ Register("/modalities/{id}/store", DicomStore); Register("/modalities/{id}/store-straight", DicomStoreStraight); // New in 1.6.1 Register("/modalities/{id}/move", DicomMove); + Register("/modalities/{id}/get", DicomGet); Register("/modalities/{id}/configuration", GetModalityConfiguration); // New in 1.8.1 // For Query/Retrieve
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -75,6 +75,7 @@ static const char* const DATABASE_BACKEND_PLUGIN = "DatabaseBackendPlugin"; static const char* const DATABASE_VERSION = "DatabaseVersion"; static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier"; + static const char* const DEFAULT_RETRIEVE_METHOD = "DefaultRetrieveMethod"; static const char* const DICOM_AET = "DicomAet"; static const char* const DICOM_PORT = "DicomPort"; static const char* const HTTP_PORT = "HttpPort"; @@ -115,6 +116,7 @@ "Information about the installed storage area plugin (`null` if no such plugin is installed)") .SetAnswerField(DATABASE_BACKEND_PLUGIN, RestApiCallDocumentation::Type_String, "Information about the installed database index plugin (`null` if no such plugin is installed)") + .SetAnswerField(DEFAULT_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String, "The DefaultRetrieveMethod configuration") .SetAnswerField(DICOM_AET, RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc") .SetAnswerField(DICOM_PORT, RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc") .SetAnswerField(HTTP_PORT, RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc") @@ -173,6 +175,7 @@ result[MAXIMUM_STORAGE_SIZE] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_STORAGE_SIZE, 0); // New in Orthanc 1.11.3 result[MAXIMUM_PATIENT_COUNT] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_PATIENT_COUNT, 0); // New in Orthanc 1.12.4 result[MAXIMUM_STORAGE_MODE] = lock.GetConfiguration().GetStringParameter(MAXIMUM_STORAGE_MODE, "Recycle"); // New in Orthanc 1.11.3 + result[DEFAULT_RETRIEVE_METHOD] = lock.GetConfiguration().GetStringParameter(DEFAULT_RETRIEVE_METHOD, "C-MOVE"); } result[STORAGE_AREA_PLUGIN] = Json::nullValue; @@ -478,6 +481,33 @@ AnswerAcceptedTransferSyntaxes(call); } + static void GetAcceptedSopClasses(RestApiGetCall& call) + { + if (call.IsDocumentation()) + { + call.GetDocumentation() + .SetTag("System") + .SetSummary("Get accepted SOPClassUID") + .SetDescription("Get the list of SOP Class UIDs that are accepted " + "by Orthanc C-STORE SCP. This corresponds to the configuration options " + "`AcceptedSopClasses` and `RejectedSopClasses`.") + .AddAnswerType(MimeType_Json, "JSON array containing the SOP Class UIDs"); + return; + } + + std::set<std::string> sopClasses; + OrthancRestApi::GetContext(call).GetAcceptedSopClasses(sopClasses, 0); + + Json::Value json = Json::arrayValue; + for (std::set<std::string>::const_iterator + sop = sopClasses.begin(); sop != sopClasses.end(); ++sop) + { + json.append(*sop); + } + + call.GetOutput().AnswerJson(json); + } + static void GetUnknownSopClassAccepted(RestApiGetCall& call) { @@ -1200,5 +1230,8 @@ Register("/tools/unknown-sop-class-accepted", SetUnknownSopClassAccepted); Register("/tools/labels", ListAllLabels); // New in Orthanc 1.12.0 + + Register("/tools/accepted-sop-classes", GetAcceptedSopClasses); + } }
--- a/OrthancServer/Sources/QueryRetrieveHandler.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/QueryRetrieveHandler.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -79,7 +79,7 @@ params.SetTimeout(timeout_); } - DicomControlUserConnection connection(params); + DicomControlUserConnection connection(params, static_cast<ScuOperationFlags>(ScuOperationFlags_Find)); connection.Find(answers_, level_, fixed, findNormalized_); }
--- a/OrthancServer/Sources/ServerContext.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -50,6 +50,9 @@ #include <dcmtk/dcmdata/dcfilefo.h> #include <dcmtk/dcmnet/dimse.h> +#include <dcmtk/dcmdata/dcuid.h> /* for variable dcmAllStorageSOPClassUIDs */ + +#include <boost/regex.hpp> #if HAVE_MALLOC_TRIM == 1 # include <malloc.h> @@ -490,6 +493,15 @@ lock.GetConfiguration().GetAcceptedTransferSyntaxes(acceptedTransferSyntaxes_); isUnknownSopClassAccepted_ = lock.GetConfiguration().GetBooleanParameter("UnknownSopClassAccepted", false); + + // New options in Orthanc 1.12.6 + std::list<std::string> acceptedSopClasses; + std::set<std::string> rejectedSopClasses; + lock.GetConfiguration().GetListOfStringsParameter(acceptedSopClasses, "AcceptedSopClasses"); + lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, "RejectSopClasses"); + SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses); + + defaultDicomRetrieveMethod_ = StringToRetrieveMethod(lock.GetConfiguration().GetStringParameter("DicomDefaultRetrieveMethod", "C-MOVE")); } jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); @@ -2009,8 +2021,118 @@ } } + void ServerContext::SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses, + const std::set<std::string>& rejectedSopClasses) + { + boost::mutex::scoped_lock lock(dynamicOptionsMutex_); + acceptedSopClasses_.clear(); - void ServerContext::GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) + size_t count = 0; + std::set<std::string> allDcmtkSopClassUids; + std::set<std::string> shortDcmtkSopClassUids; + + // we actually take a list of default 120 most common storage SOP classes defined in DCMTK + while (dcmLongSCUStorageSOPClassUIDs[count] != NULL) + { + shortDcmtkSopClassUids.insert(dcmLongSCUStorageSOPClassUIDs[count++]); + } + + count = 0; + while (dcmAllStorageSOPClassUIDs[count] != NULL) + { + allDcmtkSopClassUids.insert(dcmAllStorageSOPClassUIDs[count++]); + } + + if (acceptedSopClasses.size() == 0) + { + // by default, include the short list first and then all the others + for (std::set<std::string>::const_iterator it = shortDcmtkSopClassUids.begin(); it != shortDcmtkSopClassUids.end(); ++it) + { + acceptedSopClasses_.push_back(*it); + } + + for (std::set<std::string>::const_iterator it = allDcmtkSopClassUids.begin(); it != allDcmtkSopClassUids.end(); ++it) + { + if (shortDcmtkSopClassUids.find(*it) == shortDcmtkSopClassUids.end()) // don't add the classes that we have already added + { + acceptedSopClasses_.push_back(*it); + } + } + } + else + { + std::set<std::string> addedSopClasses; + + for (std::list<std::string>::const_iterator it = acceptedSopClasses.begin(); it != acceptedSopClasses.end(); ++it) + { + if (it->find('*') != std::string::npos || it->find('?') != std::string::npos) + { + // if it contains wildcard, add all the matching SOP classes known by DCMTK + boost::regex pattern(Toolbox::WildcardToRegularExpression(*it)); + + for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall) + { + if (regex_match(*itall, pattern) && addedSopClasses.find(*itall) == addedSopClasses.end()) + { + acceptedSopClasses_.push_back(*itall); + addedSopClasses.insert(*itall); + } + } + } + else + { + // if it is a SOP Class UID, add it without checking if it is known by DCMTK + acceptedSopClasses_.push_back(*it); + addedSopClasses.insert(*it); + } + } + } + + // now remove all rejected syntaxes + if (rejectedSopClasses.size() > 0) + { + for (std::set<std::string>::const_iterator it = rejectedSopClasses.begin(); it != rejectedSopClasses.end(); ++it) + { + if (it->find('*') != std::string::npos || it->find('?') != std::string::npos) + { + // if it contains wildcard, get all the matching SOP classes known by DCMTK + boost::regex pattern(Toolbox::WildcardToRegularExpression(*it)); + + for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall) + { + if (regex_match(*itall, pattern)) + { + acceptedSopClasses_.remove(*itall); + } + } + } + else + { + // if it is a SOP Class UID, remove it without checking if it is known by DCMTK + acceptedSopClasses_.remove(*it); + } + } + } + } + + void ServerContext::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const + { + sopClasses.clear(); + + boost::mutex::scoped_lock lock(dynamicOptionsMutex_); + + size_t count = 0; + std::list<std::string>::const_iterator it = acceptedSopClasses_.begin(); + + while (it != acceptedSopClasses_.end() && (maxCount == 0 || count < maxCount)) + { + sopClasses.insert(*it); + count++; + it++; + } + } + + void ServerContext::GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) const { boost::mutex::scoped_lock lock(dynamicOptionsMutex_); syntaxes = acceptedTransferSyntaxes_; @@ -2024,7 +2146,25 @@ } - bool ServerContext::IsUnknownSopClassAccepted() + void ServerContext::GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& syntaxes) const + { + boost::mutex::scoped_lock lock(dynamicOptionsMutex_); + + // // TODO: investigate: actually, neither Orthanc 1.12.4 nor DCM4CHEE will accept to send a LittleEndianExplicit file + // // while e.g., Jpeg-LS has been presented (and accepted) as the preferred TS for the C-Store SCP. + // // if we have defined IngestTranscoding, let's propose this TS first to avoid any unnecessary transcoding + // if (isIngestTranscoding_) + // { + // syntaxes.push_back(ingestTransferSyntax_); + // } + + // then, propose the default ones + syntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit); + syntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit); + } + + + bool ServerContext::IsUnknownSopClassAccepted() const { boost::mutex::scoped_lock lock(dynamicOptionsMutex_); return isUnknownSopClassAccepted_;
--- a/OrthancServer/Sources/ServerContext.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.h Thu Jan 16 16:40:08 2025 +0100 @@ -233,6 +233,7 @@ std::unique_ptr<SharedArchive> queryRetrieveArchive_; std::string defaultLocalAet_; + RetrieveMethod defaultDicomRetrieveMethod_; OrthancHttpHandler httpHandler_; bool saveJobs_; FindStorageAccessMode findStorageAccessMode_; @@ -257,9 +258,10 @@ // New in Orthanc 1.9.0 DicomTransferSyntax preferredTransferSyntax_; - boost::mutex dynamicOptionsMutex_; + mutable boost::mutex dynamicOptionsMutex_; bool isUnknownSopClassAccepted_; std::set<DicomTransferSyntax> acceptedTransferSyntaxes_; + std::list<std::string> acceptedSopClasses_; // ordered; the most 120 common ones first bool readOnly_; StoreResult StoreAfterTranscoding(std::string& resultPublicId, @@ -436,6 +438,11 @@ return defaultLocalAet_; } + RetrieveMethod GetDefaultDicomRetrieveMethod() const + { + return defaultDicomRetrieveMethod_; + } + LuaScripting& GetLuaScripting() { return mainLua_; @@ -583,11 +590,18 @@ const std::string& GetDeidentifiedContent(const DicomElement& element) const; - void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes); + void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) const; void SetAcceptedTransferSyntaxes(const std::set<DicomTransferSyntax>& syntaxes); - bool IsUnknownSopClassAccepted(); + void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& syntaxes) const; + + void SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses, + const std::set<std::string>& rejectedSopClasses); + + void GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const; + + bool IsUnknownSopClassAccepted() const; void SetUnknownSopClassAccepted(bool accepted);
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -1518,7 +1518,7 @@ } - float ArchiveJob::GetProgress() + float ArchiveJob::GetProgress() const { if (writer_.get() == NULL || writer_->GetStepsCount() == 0) @@ -1533,7 +1533,7 @@ } - void ArchiveJob::GetJobType(std::string& target) + void ArchiveJob::GetJobType(std::string& target) const { if (isMedia_) { @@ -1546,7 +1546,7 @@ } - void ArchiveJob::GetPublicContent(Json::Value& value) + void ArchiveJob::GetPublicContent(Json::Value& value) const { value = Json::objectValue; value[KEY_DESCRIPTION] = description_;
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -107,13 +107,13 @@ virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - virtual float GetProgress() ORTHANC_OVERRIDE; + virtual float GetProgress() const ORTHANC_OVERRIDE; - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE; + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE; - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE + virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE { return false; // Cannot serialize this kind of job }
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -83,7 +83,7 @@ } - bool CleaningInstancesJob::Serialize(Json::Value& target) + bool CleaningInstancesJob::Serialize(Json::Value& target) const { if (!SetOfInstancesJob::Serialize(target)) {
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -62,7 +62,7 @@ void SetKeepSource(bool keep); - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; virtual void Start() ORTHANC_OVERRIDE; };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -0,0 +1,121 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "DicomGetScuJob.h" + +#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../../OrthancFramework/Sources/SerializationToolbox.h" +#include "../ServerContext.h" +#include <dcmtk/dcmnet/dimse.h> +#include <algorithm> + +static const char* const LOCAL_AET = "LocalAet"; +static const char* const QUERY = "Query"; +static const char* const QUERY_FORMAT = "QueryFormat"; // New in 1.9.5 +static const char* const REMOTE = "Remote"; +static const char* const TIMEOUT = "Timeout"; + +namespace Orthanc +{ + + + static uint16_t InstanceReceivedHandler(void* callbackContext, + DcmDataset& dataset, + const std::string& remoteAet, + const std::string& remoteIp, + const std::string& calledAet) + { + // this code is equivalent to OrthancStoreRequestHandler + ServerContext* context = reinterpret_cast<ServerContext*>(callbackContext); + + std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromDcmDataset(dataset)); + + if (toStore->GetBufferSize() > 0) + { + toStore->SetOrigin(DicomInstanceOrigin::FromDicomProtocol + (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str())); + + std::string id; + ServerContext::StoreResult result = context->Store(id, *toStore, StoreInstanceMode_Default); + return result.GetCStoreStatusCode(); + } + + return STATUS_STORE_Error_CannotUnderstand; + } + + void DicomGetScuJob::Retrieve(const DicomMap& findAnswer) + { + if (connection_.get() == NULL) + { + std::set<std::string> sopClassesToPropose; + std::set<std::string> acceptedSopClasses; + std::list<DicomTransferSyntax> proposedTransferSyntaxes; + + if (sopClassesFromResourcesToRetrieve_.size() > 0) + { + context_.GetAcceptedSopClasses(acceptedSopClasses, 0); + + // keep the sop classes from the resources to retrieve only if they are accepted by Orthanc + Toolbox::GetIntersection(sopClassesToPropose, sopClassesFromResourcesToRetrieve_, acceptedSopClasses); + } + else + { + // when we don't know what SOP Classes to use, we include the 120 most common SOP Classes because + // there are only 128 presentation contexts available + context_.GetAcceptedSopClasses(sopClassesToPropose, 120); + } + + if (sopClassesToPropose.size() == 0) + { + throw OrthancException(ErrorCode_NoPresentationContext, "Cannot perform C-Get, no SOPClassUID have been accepted by Orthanc."); + } + + context_.GetProposedStorageTransferSyntaxes(proposedTransferSyntaxes); + + connection_.reset(new DicomControlUserConnection(parameters_, + ScuOperationFlags_Get, + sopClassesToPropose, + proposedTransferSyntaxes)); + } + + connection_->SetProgressListener(this); + connection_->Get(findAnswer, InstanceReceivedHandler, &context_); + } + + void DicomGetScuJob::AddFindAnswer(const DicomMap& answer) + { + DicomRetrieveScuBaseJob::AddFindAnswer(answer); + + std::set<std::string> sopClassesInStudy; + if (answer.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) + && answer.LookupStringValues(sopClassesInStudy, DICOM_TAG_SOP_CLASSES_IN_STUDY, false)) + { + for (std::set<std::string>::const_iterator it = sopClassesInStudy.begin(); it != sopClassesInStudy.end(); ++it) + { + sopClassesFromResourcesToRetrieve_.insert(*it); + } + } + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -0,0 +1,60 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h" +#include "DicomRetrieveScuBaseJob.h" +#include "../QueryRetrieveHandler.h" + +namespace Orthanc +{ + class ServerContext; + + class DicomGetScuJob : public DicomRetrieveScuBaseJob + { + private: + std::set<std::string> sopClassesFromResourcesToRetrieve_; + + virtual void Retrieve(const DicomMap& findAnswer) ORTHANC_OVERRIDE; + + public: + explicit DicomGetScuJob(ServerContext& context) : + DicomRetrieveScuBaseJob(context) + { + } + + DicomGetScuJob(ServerContext& context, + const Json::Value& serialized); + + + virtual void AddFindAnswer(const DicomMap &answer) ORTHANC_OVERRIDE; + + + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE + { + target = "DicomGetScu"; + } + }; +}
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -242,7 +242,7 @@ } - void DicomModalityStoreJob::GetPublicContent(Json::Value& value) + void DicomModalityStoreJob::GetPublicContent(Json::Value& value) const { SetOfInstancesJob::GetPublicContent(value); @@ -281,7 +281,7 @@ } - bool DicomModalityStoreJob::Serialize(Json::Value& target) + bool DicomModalityStoreJob::Serialize(Json::Value& target) const { if (!SetOfInstancesJob::Serialize(target)) {
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -88,14 +88,14 @@ virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE { target = "DicomModalityStore"; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; virtual void Reset() ORTHANC_OVERRIDE;
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -36,123 +36,21 @@ namespace Orthanc { - class DicomMoveScuJob::Command : public SetOfCommandsJob::ICommand - { - private: - DicomMoveScuJob& that_; - std::unique_ptr<DicomMap> findAnswer_; - public: - Command(DicomMoveScuJob& that, - const DicomMap& findAnswer) : - that_(that), - findAnswer_(findAnswer.Clone()) - { - } - - virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE - { - that_.Retrieve(*findAnswer_); - return true; - } - - virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE - { - findAnswer_->Serialize(target); - } - }; - - - class DicomMoveScuJob::Unserializer : - public SetOfCommandsJob::ICommandUnserializer - { - private: - DicomMoveScuJob& that_; - - public: - explicit Unserializer(DicomMoveScuJob& that) : - that_(that) - { - } - - virtual ICommand* Unserialize(const Json::Value& source) const ORTHANC_OVERRIDE - { - DicomMap findAnswer; - findAnswer.Unserialize(source); - return new Command(that_, findAnswer); - } - }; void DicomMoveScuJob::Retrieve(const DicomMap& findAnswer) { if (connection_.get() == NULL) { - connection_.reset(new DicomControlUserConnection(parameters_)); + connection_.reset(new DicomControlUserConnection(parameters_, ScuOperationFlags_Move)); } + connection_->SetProgressListener(this); connection_->Move(targetAet_, findAnswer); } - static void AddToQuery(DicomFindAnswers& query, - const DicomMap& item) - { - query.Add(item); - - /** - * Compatibility with Orthanc <= 1.9.4: Remove the - * "SpecificCharacterSet" (0008,0005) tag that is automatically - * added if creating a ParsedDicomFile object from a DicomMap. - **/ - query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); - } - - // this method is used to implement the retrieve part of a Q&R - // it keeps only the main dicom tags from the C-Find answer - void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer) - { - DicomMap item; - item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL); - item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID); - item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID); - item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID); - item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID); - item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER); - AddToQuery(query_, item); - - AddCommand(new Command(*this, answer)); - } - - // this method is used to implement a C-Move - // it keeps all tags from the C-Move query - void DicomMoveScuJob::AddQuery(const DicomMap& query) - { - AddToQuery(query_, query); - AddCommand(new Command(*this, query)); - } - - void DicomMoveScuJob::AddFindAnswer(QueryRetrieveHandler& query, - size_t i) - { - DicomMap answer; - query.GetAnswer(answer, i); - AddFindAnswer(answer); - } - - - void DicomMoveScuJob::SetLocalAet(const std::string& aet) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - parameters_.SetLocalApplicationEntityTitle(aet); - } - } - void DicomMoveScuJob::SetTargetAet(const std::string& aet) { @@ -167,109 +65,33 @@ } - void DicomMoveScuJob::SetRemoteModality(const RemoteModalityParameters& remote) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - parameters_.SetRemoteModality(remote); - } - } - void DicomMoveScuJob::SetTimeout(uint32_t seconds) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - parameters_.SetTimeout(seconds); - } - } - - - void DicomMoveScuJob::Stop(JobStopReason reason) - { - connection_.reset(); - } - - - void DicomMoveScuJob::SetQueryFormat(DicomToJsonFormat format) + void DicomMoveScuJob::GetPublicContent(Json::Value& value) const { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - queryFormat_ = format; - } - } + DicomRetrieveScuBaseJob::GetPublicContent(value); - - void DicomMoveScuJob::GetPublicContent(Json::Value& value) - { - SetOfCommandsJob::GetPublicContent(value); - - value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle(); - value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle(); - - value[QUERY] = Json::objectValue; - query_.ToJson(value[QUERY], queryFormat_); + value[TARGET_AET] = targetAet_; } DicomMoveScuJob::DicomMoveScuJob(ServerContext& context, const Json::Value& serialized) : - SetOfCommandsJob(new Unserializer(*this), serialized), - context_(context), - parameters_(DicomAssociationParameters::UnserializeJob(serialized)), - targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET)), - query_(true), - queryFormat_(DicomToJsonFormat_Short) + DicomRetrieveScuBaseJob(context, serialized), + targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET)) { - if (serialized.isMember(QUERY)) - { - const Json::Value& query = serialized[QUERY]; - if (query.type() == Json::arrayValue) - { - for (Json::Value::ArrayIndex i = 0; i < query.size(); i++) - { - DicomMap item; - FromDcmtkBridge::FromJson(item, query[i]); - AddToQuery(query_, item); - } - } - } - - if (serialized.isMember(QUERY_FORMAT)) - { - queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT)); - } } - bool DicomMoveScuJob::Serialize(Json::Value& target) + bool DicomMoveScuJob::Serialize(Json::Value& target) const { - if (!SetOfCommandsJob::Serialize(target)) + if (!DicomRetrieveScuBaseJob::Serialize(target)) { return false; } else { - parameters_.SerializeJob(target); target[TARGET_AET] = targetAet_; - - // "Short" is for compatibility with Orthanc <= 1.9.4 - target[QUERY] = Json::objectValue; - query_.ToJson(target[QUERY], DicomToJsonFormat_Short); - - target[QUERY_FORMAT] = EnumerationToString(queryFormat_); return true; }
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -25,7 +25,8 @@ #include "../../../OrthancFramework/Sources/Compatibility.h" #include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h" -#include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h" +// #include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h" +#include "DicomRetrieveScuBaseJob.h" #include "../QueryRetrieveHandler.h" @@ -33,51 +34,22 @@ { class ServerContext; - class DicomMoveScuJob : public SetOfCommandsJob + class DicomMoveScuJob : public DicomRetrieveScuBaseJob { private: - class Command; - class Unserializer; + std::string targetAet_; - ServerContext& context_; - DicomAssociationParameters parameters_; - std::string targetAet_; - DicomFindAnswers query_; - DicomToJsonFormat queryFormat_; // New in 1.9.5 - - std::unique_ptr<DicomControlUserConnection> connection_; - - void Retrieve(const DicomMap& findAnswer); + virtual void Retrieve(const DicomMap& findAnswer) ORTHANC_OVERRIDE; public: explicit DicomMoveScuJob(ServerContext& context) : - context_(context), - query_(false /* this is not for worklists */), - queryFormat_(DicomToJsonFormat_Short) + DicomRetrieveScuBaseJob(context) { } DicomMoveScuJob(ServerContext& context, const Json::Value& serialized); - void AddFindAnswer(const DicomMap& answer); - - void AddQuery(const DicomMap& query); - - void AddFindAnswer(QueryRetrieveHandler& query, - size_t i); - - const DicomAssociationParameters& GetParameters() const - { - return parameters_; - } - - void SetLocalAet(const std::string& aet); - - void SetRemoteModality(const RemoteModalityParameters& remote); - - void SetTimeout(uint32_t timeout); - const std::string& GetTargetAet() const { return targetAet_; @@ -85,22 +57,13 @@ void SetTargetAet(const std::string& aet); - void SetQueryFormat(DicomToJsonFormat format); - - DicomToJsonFormat GetQueryFormat() const - { - return queryFormat_; - } - - virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE { target = "DicomMoveScu"; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -0,0 +1,236 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "DicomGetScuJob.h" + +#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../../OrthancFramework/Sources/SerializationToolbox.h" +#include "../ServerContext.h" +#include <dcmtk/dcmnet/dimse.h> +#include <algorithm> +#include "../../../OrthancFramework/Sources/Logging.h" + +static const char* const LOCAL_AET = "LocalAet"; +static const char* const QUERY = "Query"; +static const char* const QUERY_FORMAT = "QueryFormat"; // New in 1.9.5 +static const char* const REMOTE = "Remote"; +static const char* const TIMEOUT = "Timeout"; + +namespace Orthanc +{ + + static void AddToQuery(DicomFindAnswers& query, + const DicomMap& item) + { + query.Add(item); + + /** + * Compatibility with Orthanc <= 1.9.4: Remove the + * "SpecificCharacterSet" (0008,0005) tag that is automatically + * added if creating a ParsedDicomFile object from a DicomMap. + **/ + query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); + } + + // this method is used to implement the retrieve part of a Q&R + // it keeps only the main dicom tags from the C-Find answer + void DicomRetrieveScuBaseJob::AddFindAnswer(const DicomMap& answer) + { + DicomMap item; + item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL); + item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID); + item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID); + item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID); + item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID); + item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER); + AddToQuery(query_, item); + + AddCommand(new Command(*this, answer)); + } + + void DicomRetrieveScuBaseJob::AddFindAnswer(QueryRetrieveHandler& query, + size_t i) + { + DicomMap answer; + query.GetAnswer(answer, i); + AddFindAnswer(answer); + } + + // this method is used to implement a C-Move + // it keeps all tags from the C-Move query + void DicomRetrieveScuBaseJob::AddQuery(const DicomMap& query) + { + AddToQuery(query_, query); + AddCommand(new Command(*this, query)); + } + + + void DicomRetrieveScuBaseJob::SetLocalAet(const std::string& aet) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetLocalApplicationEntityTitle(aet); + } + } + + + void DicomRetrieveScuBaseJob::SetRemoteModality(const RemoteModalityParameters& remote) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetRemoteModality(remote); + } + } + + + void DicomRetrieveScuBaseJob::SetTimeout(uint32_t seconds) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetTimeout(seconds); + } + } + + + void DicomRetrieveScuBaseJob::Stop(JobStopReason reason) + { + connection_.reset(); + } + + + void DicomRetrieveScuBaseJob::SetQueryFormat(DicomToJsonFormat format) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + queryFormat_ = format; + } + } + + + void DicomRetrieveScuBaseJob::GetPublicContent(Json::Value& value) const + { + SetOfCommandsJob::GetPublicContent(value); + + value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle(); + value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle(); + + value[QUERY] = Json::objectValue; + query_.ToJson(value[QUERY], queryFormat_); + } + + + DicomRetrieveScuBaseJob::DicomRetrieveScuBaseJob(ServerContext& context, + const Json::Value& serialized) : + SetOfCommandsJob(new Unserializer(*this), serialized), + context_(context), + parameters_(DicomAssociationParameters::UnserializeJob(serialized)), + query_(true), + queryFormat_(DicomToJsonFormat_Short), + nbRemainingSubOperations_(0), + nbCompletedSubOperations_(0), + nbFailedSubOperations_(0), + nbWarningSubOperations_(0) + { + if (serialized.isMember(QUERY)) + { + const Json::Value& query = serialized[QUERY]; + if (query.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < query.size(); i++) + { + DicomMap item; + FromDcmtkBridge::FromJson(item, query[i]); + AddToQuery(query_, item); + } + } + } + + if (serialized.isMember(QUERY_FORMAT)) + { + queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT)); + } + } + + + bool DicomRetrieveScuBaseJob::Serialize(Json::Value& target) const + { + if (!SetOfCommandsJob::Serialize(target)) + { + return false; + } + else + { + parameters_.SerializeJob(target); + + // "Short" is for compatibility with Orthanc <= 1.9.4 + target[QUERY] = Json::objectValue; + query_.ToJson(target[QUERY], DicomToJsonFormat_Short); + + target[QUERY_FORMAT] = EnumerationToString(queryFormat_); + + return true; + } + } + + void DicomRetrieveScuBaseJob::OnProgressUpdated(uint16_t nbRemainingSubOperations, + uint16_t nbCompletedSubOperations, + uint16_t nbFailedSubOperations, + uint16_t nbWarningSubOperations) + { + boost::mutex::scoped_lock lock(progressMutex_); + + nbRemainingSubOperations_ = nbRemainingSubOperations; + nbCompletedSubOperations_ = nbCompletedSubOperations; + nbFailedSubOperations_ = nbFailedSubOperations; + nbWarningSubOperations_ = nbWarningSubOperations; + } + + float DicomRetrieveScuBaseJob::GetProgress() const + { + boost::mutex::scoped_lock lock(progressMutex_); + + uint32_t totalOperations = nbRemainingSubOperations_ + nbCompletedSubOperations_ + nbFailedSubOperations_ + nbWarningSubOperations_; + if (totalOperations == 0) + { + return 0.0f; + } + + return float(nbCompletedSubOperations_ + nbFailedSubOperations_ + nbWarningSubOperations_) / float(totalOperations); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -0,0 +1,154 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + +#pragma once + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h" +#include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h" +#include <boost/thread/mutex.hpp> + +#include "../QueryRetrieveHandler.h" + +namespace Orthanc +{ + class ServerContext; + + class DicomRetrieveScuBaseJob : public SetOfCommandsJob, public DicomControlUserConnection::IProgressListener + { + protected: + class Command : public SetOfCommandsJob::ICommand + { + private: + DicomRetrieveScuBaseJob &that_; + std::unique_ptr<DicomMap> findAnswer_; + + public: + Command(DicomRetrieveScuBaseJob &that, + const DicomMap &findAnswer) : + that_(that), + findAnswer_(findAnswer.Clone()) + { + } + + virtual bool Execute(const std::string &jobId) ORTHANC_OVERRIDE + { + that_.Retrieve(*findAnswer_); + return true; + } + + virtual void Serialize(Json::Value &target) const ORTHANC_OVERRIDE + { + findAnswer_->Serialize(target); + } + }; + + class Unserializer : public SetOfCommandsJob::ICommandUnserializer + { + protected: + DicomRetrieveScuBaseJob &that_; + + public: + explicit Unserializer(DicomRetrieveScuBaseJob &that) : + that_(that) + { + } + + virtual ICommand *Unserialize(const Json::Value &source) const ORTHANC_OVERRIDE + { + DicomMap findAnswer; + findAnswer.Unserialize(source); + return new Command(that_, findAnswer); + } + }; + + ServerContext &context_; + DicomAssociationParameters parameters_; + DicomFindAnswers query_; + DicomToJsonFormat queryFormat_; // New in 1.9.5 + + std::unique_ptr<DicomControlUserConnection> connection_; + + mutable boost::mutex progressMutex_; + uint16_t nbRemainingSubOperations_; + uint16_t nbCompletedSubOperations_; + uint16_t nbFailedSubOperations_; + uint16_t nbWarningSubOperations_; + + virtual void Retrieve(const DicomMap &findAnswer) = 0; + + explicit DicomRetrieveScuBaseJob(ServerContext &context) : + context_(context), + query_(false /* this is not for worklists */), + queryFormat_(DicomToJsonFormat_Short) + { + } + + DicomRetrieveScuBaseJob(ServerContext &context, + const Json::Value &serialized); + + public: + virtual void AddFindAnswer(const DicomMap &answer); + + void AddQuery(const DicomMap& query); + + void AddFindAnswer(QueryRetrieveHandler &query, + size_t i); + + const DicomAssociationParameters &GetParameters() const + { + return parameters_; + } + + void SetLocalAet(const std::string &aet); + + void SetRemoteModality(const RemoteModalityParameters &remote); + + void SetTimeout(uint32_t timeout); + + void SetQueryFormat(DicomToJsonFormat format); + + DicomToJsonFormat GetQueryFormat() const + { + return queryFormat_; + } + + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; + + virtual void GetPublicContent(Json::Value &value) const ORTHANC_OVERRIDE; + + virtual bool Serialize(Json::Value &target) const ORTHANC_OVERRIDE; + + virtual void OnProgressUpdated(uint16_t nbRemainingSubOperations, + uint16_t nbCompletedSubOperations, + uint16_t nbFailedSubOperations, + uint16_t nbWarningSubOperations) ORTHANC_OVERRIDE; + + virtual float GetProgress() const ORTHANC_OVERRIDE; + + virtual bool NeedsProgressUpdateBetweenSteps() const ORTHANC_OVERRIDE + { + return true; + } + + }; +}
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -353,7 +353,7 @@ } - void MergeStudyJob::GetPublicContent(Json::Value& value) + void MergeStudyJob::GetPublicContent(Json::Value& value) const { CleaningInstancesJob::GetPublicContent(value); value["TargetStudy"] = targetStudy_; @@ -386,7 +386,7 @@ } - bool MergeStudyJob::Serialize(Json::Value& target) + bool MergeStudyJob::Serialize(Json::Value& target) const { if (!CleaningInstancesJob::Serialize(target)) {
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -89,13 +89,13 @@ { } - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE { target = "MergeStudy"; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -220,7 +220,7 @@ } - void OrthancPeerStoreJob::GetPublicContent(Json::Value& value) + void OrthancPeerStoreJob::GetPublicContent(Json::Value& value) const { SetOfInstancesJob::GetPublicContent(value); @@ -285,7 +285,7 @@ } - bool OrthancPeerStoreJob::Serialize(Json::Value& target) + bool OrthancPeerStoreJob::Serialize(Json::Value& target) const { if (!SetOfInstancesJob::Serialize(target)) {
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -92,13 +92,13 @@ virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; // For pausing jobs - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE { target = "OrthancPeerStore"; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -542,7 +542,7 @@ } - void ResourceModificationJob::GetPublicContent(Json::Value& value) + void ResourceModificationJob::GetPublicContent(Json::Value& value) const { boost::recursive_mutex::scoped_lock lock(outputMutex_); @@ -636,7 +636,7 @@ } } - bool ResourceModificationJob::Serialize(Json::Value& value) + bool ResourceModificationJob::Serialize(Json::Value& value) const { if (modification_.get() == NULL) {
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -116,14 +116,14 @@ // Only possible if "IsSingleResourceModification()" ResourceType GetOutputLevel() const; - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE { target = "ResourceModification"; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE; virtual void Reset() ORTHANC_OVERRIDE;
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -306,7 +306,7 @@ } - void SplitStudyJob::GetPublicContent(Json::Value& value) + void SplitStudyJob::GetPublicContent(Json::Value& value) const { CleaningInstancesJob::GetPublicContent(value); @@ -351,7 +351,7 @@ } - bool SplitStudyJob::Serialize(Json::Value& target) + bool SplitStudyJob::Serialize(Json::Value& target) const { if (!CleaningInstancesJob::Serialize(target)) {
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -113,13 +113,13 @@ { } - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE { target = "SplitStudy"; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -404,7 +404,7 @@ } - void StorageCommitmentScpJob::GetPublicContent(Json::Value& value) + void StorageCommitmentScpJob::GetPublicContent(Json::Value& value) const { SetOfCommandsJob::GetPublicContent(value); @@ -434,7 +434,7 @@ } - bool StorageCommitmentScpJob::Serialize(Json::Value& target) + bool StorageCommitmentScpJob::Serialize(Json::Value& target) const { if (!SetOfCommandsJob::Serialize(target)) {
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -89,13 +89,13 @@ { } - virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE { target = "StorageCommitmentScp"; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -376,7 +376,7 @@ static const char* KEY_WORKERS_COUNT = "WorkersCount"; - void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target) + void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target) const { boost::recursive_mutex::scoped_lock lock(mutex_); @@ -391,7 +391,7 @@ } - bool ThreadedSetOfInstancesJob::Serialize(Json::Value& target) + bool ThreadedSetOfInstancesJob::Serialize(Json::Value& target) const { boost::recursive_mutex::scoped_lock lock(mutex_); @@ -478,7 +478,7 @@ return keepSource_; } - float ThreadedSetOfInstancesJob::GetProgress() + float ThreadedSetOfInstancesJob::GetProgress() const { boost::recursive_mutex::scoped_lock lock(mutex_);
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Thu Jan 16 16:40:08 2025 +0100 @@ -140,15 +140,15 @@ virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - virtual float GetProgress() ORTHANC_OVERRIDE; + virtual float GetProgress() const ORTHANC_OVERRIDE; bool IsStarted() const; virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE; virtual bool GetOutput(std::string& output, MimeType& mime,
--- a/OrthancServer/Sources/main.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/Sources/main.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -496,6 +496,13 @@ context_.GetAcceptedTransferSyntaxes(target); } + virtual void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& target, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) ORTHANC_OVERRIDE + { + context_.GetProposedStorageTransferSyntaxes(target); + } virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp, const std::string& remoteAet, @@ -503,6 +510,12 @@ { return context_.IsUnknownSopClassAccepted(); } + + virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses, + size_t maxCount) ORTHANC_OVERRIDE + { + context_.GetAcceptedSopClasses(sopClasses, maxCount); + } }; @@ -889,6 +902,7 @@ PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists"); PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)"); PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP"); + PrintErrorCode(ErrorCode_DicomGetUnavailable, "DicomUserConnection: The C-GET command is not supported by the remote SCP"); PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type"); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -0,0 +1,150 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "PrecompiledHeadersUnitTests.h" +#include <gtest/gtest.h> + +#include "../../OrthancFramework/Sources/Compatibility.h" +#include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h" +#include "../../OrthancFramework/Sources/Logging.h" +#include "../../OrthancFramework/Sources/SerializationToolbox.h" + +#include "../Sources/Database/SQLiteDatabaseWrapper.h" +#include "../Sources/ServerContext.h" + +using namespace Orthanc; + +TEST(ServerConfig, AcceptedSopClasses) +{ + const std::string path = "UnitTestsStorage"; + + MemoryStorageArea storage; + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + ServerContext context(db, storage, true /* running unit tests */, 10, false, 1); + + { // default config -> all SOP Classes should be accepted + std::set<std::string> s; + + context.GetAcceptedSopClasses(s, 0); + ASSERT_LE(100u, s.size()); + + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end()); + + context.GetAcceptedSopClasses(s, 1); + ASSERT_EQ(1u, s.size()); + } + + { + std::list<std::string> acceptedStorageClasses; + std::set<std::string> rejectedStorageClasses; + + std::set<std::string> s; + context.GetAcceptedSopClasses(s, 0); + size_t allSize = s.size(); + + { // default config but reject one class + acceptedStorageClasses.clear(); + rejectedStorageClasses.clear(); + rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.4"); + + context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); + + context.GetAcceptedSopClasses(s, 0); + ASSERT_EQ(allSize - 1, s.size()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end()); + + context.GetAcceptedSopClasses(s, 1); + ASSERT_EQ(1u, s.size()); + } + + { // default config but reject one regex + acceptedStorageClasses.clear(); + rejectedStorageClasses.clear(); + rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.*"); + context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); + + context.GetAcceptedSopClasses(s, 0); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end()); + } + + { // accept a single - no rejection + acceptedStorageClasses.clear(); + acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.1.4"); + rejectedStorageClasses.clear(); + context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); + + context.GetAcceptedSopClasses(s, 0); + ASSERT_EQ(1, s.size()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end()); + } + + { // accept from regex - reject one + acceptedStorageClasses.clear(); + acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*"); + rejectedStorageClasses.clear(); + rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.1"); + context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); + + context.GetAcceptedSopClasses(s, 0); + ASSERT_LE(10, s.size()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end()); + } + + { // accept from regex - reject from regex + acceptedStorageClasses.clear(); + acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*"); + rejectedStorageClasses.clear(); + rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.*"); + context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); + + context.GetAcceptedSopClasses(s, 0); + ASSERT_LE(10, s.size()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end()); + ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end()); + } + + { // accept one that is unknown form DCMTK + acceptedStorageClasses.clear(); + acceptedStorageClasses.push_back("1.2.3.4"); + rejectedStorageClasses.clear(); + context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); + + context.GetAcceptedSopClasses(s, 0); + ASSERT_EQ(1, s.size()); + ASSERT_TRUE(s.find("1.2.3.4") != s.end()); + } + + } + + context.Stop(); + db.Close(); +}
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Thu Jan 16 15:49:50 2025 +0100 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Thu Jan 16 16:40:08 2025 +0100 @@ -106,24 +106,24 @@ { } - virtual float GetProgress() ORTHANC_OVERRIDE + virtual float GetProgress() const ORTHANC_OVERRIDE { return static_cast<float>(count_) / static_cast<float>(steps_ - 1); } - virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE { type = "DummyJob"; } - virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE + virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE { value = Json::objectValue; value["Type"] = "DummyJob"; return true; } - virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE { value["hello"] = "world"; } @@ -202,7 +202,7 @@ { } - virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE + virtual void GetJobType(std::string& s) const ORTHANC_OVERRIDE { s = "DummyInstancesJob"; }
--- a/TODO Thu Jan 16 15:49:50 2025 +0100 +++ b/TODO Thu Jan 16 16:40:08 2025 +0100 @@ -1,3 +1,11 @@ +current work on C-Get SCU: +- for the negotiation, limit SOPClassUID to the ones listed in a C-Find response or to a list provided in the Rest API ? +- SetupPresentationContexts +- handle progress +- handle cancellation when the job is cancelled ? + + + ======================= === Orthanc Roadmap === ======================= @@ -32,6 +40,11 @@ * Add configurations to enable/disable warnings: - Modifying an instance while keeping its original SOPInstanceUID: This should be avoided! - Modifying a study while keeping its original StudyInstanceUID: This should be avoided! + In order to be able to disable/enable warnings in both the server and the framework, we should add a map of + enabled warnings in the logging classes directly and have something like: + LOG_WARNING_IF_ENABLED("Warning_ID") << ... + ENABLE_WARNING("Warning_ID", true) + Warnings from Framework should have a separate range like W999_ or WF001_ ... * Store the job registry in a dedicatd table in DB ? https://discourse.orthanc-server.org/t/performance-issue-when-adding-a-lot-of-jobs-in-the-queue/3915/2 Note: that might also be the right time to have a central jobs registry when working @@ -173,9 +186,12 @@ Mid-term -------- -* Support C-GET SCU (note that C-GET SCP was introduced in Orthanc 1.7.0) -* Configure the list of accepted SOP Classes - https://discourse.orthanc-server.org/t/can-you-limit-the-sop-classes-accepted-as-store-scp/4606 +* Check how Orthanc shall behave wrt to AcceptedSopClasses in these situations (consider Orthanc + accepts CT but not PT) + - What shall we log/warn if an external modality tries to send a PT/CT + - What shall we log/warn if we try to C-GET a PT/CT + Should the rejected files be logged as Failed, Warning, Refused, ... + Note: some tentative work has been initiated in the get-scu-test branch. * Support "Retrieve AE Title" (0008,0054) in C-FIND: - On SCP side: done by https://orthanc.uclouvain.be/hg/orthanc/rev/1ec3e1e18f50 - On SCU side: @@ -185,9 +201,6 @@ * Strict hierarchical C-FIND: https://groups.google.com/d/msg/orthanc-users/VBHpeGVSNKM/tkaVvjWFBwAJ * report DIMSE error codes in Rest API and job status for /store /query /move /retrieve -* report progress report of C-Move operation in jop progress. There are - progress callbacks available in DIMSE_moveUser - https://groups.google.com/g/orthanc-users/c/c8cGnA7FzsE/m/BSs66D8wBwAJ * Log outgoing C-Find queries * Support other Transfer Syntaxes in the Worklist plugin: https://discourse.orthanc-server.org/t/could-you-please-create-an-option-to-set-the-transfer-syntax-in-the-worklist-plugin-currently-little-endian-explicit-is-fixed/4871