Mercurial > hg > orthanc
changeset 5853:4d932683049d get-scu
very first implementation of C-Get SCU
line wrap: on
line diff
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Tue Oct 29 17:25:49 2024 +0100 @@ -602,6 +602,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/DicomNetworking/DicomAssociation.cpp Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp Tue Oct 29 17:25:49 2024 +0100 @@ -526,6 +526,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 {
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h Tue Oct 29 17:25:49 2024 +0100 @@ -122,6 +122,10 @@ 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 Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Tue Oct 29 17:25:49 2024 +0100 @@ -234,7 +234,7 @@ - void DicomControlUserConnection::SetupPresentationContexts() + void DicomControlUserConnection::SetupPresentationContexts() // TODO-GET, setup only the presentation contexts that are enabled for that modality { assert(association_.get() != NULL); association_->ProposeGenericPresentationContext(UID_VerificationSOPClass); @@ -243,6 +243,12 @@ association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel); association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel); association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel); + association_->ProposeGenericPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel); + association_->ProposeGenericPresentationContext(UID_GETPatientRootQueryRetrieveInformationModel); + + // for C-GET SCU, in order to receive the C-Store message TODO-GET: we need to refine this list based on what we know we are going to retrieve + association_->ProposeGenericPresentationContext(UID_ComputedRadiographyImageStorage); + association_->ProposeGenericPresentationContext(UID_MRImageStorage); } @@ -445,6 +451,220 @@ } + void DicomControlUserConnection::Get(const DicomMap& findResult, + CGetInstanceReceivedCallback instanceReceivedCallback, + void* callbackContext) + { + assert(association_.get() != NULL); + association_->Open(parameters_); + + // TODO-GET: if findResults is the result of a C-Find, we can use the SopClassUIDs for the negotiation + + 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; + // DU_putStringDOElement(queryDataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(ResourceType_Patient)); // TODO-GET + break; + case ResourceType_Study: + sopClass = UID_GETStudyRootQueryRetrieveInformationModel; + // DU_putStringDOElement(queryDataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(ResourceType_Study)); // TODO-GET + break; + default: + throw OrthancException(ErrorCode_InternalError); // TODO-GET: implement series + instances + } + + // 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 /* progress callback TODO-GET */, NULL /* callback context */, 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); + } + + // TODO-GET: for progress handler + // OFunique_ptr<RetrieveResponse> getRSP(new RetrieveResponse()); + // getRSP->m_affectedSOPClassUID = rsp.msg.CGetRSP.AffectedSOPClassUID; + // getRSP->m_messageIDRespondedTo = rsp.msg.CGetRSP.MessageIDBeingRespondedTo; + // getRSP->m_status = rsp.msg.CGetRSP.DimseStatus; + // getRSP->m_numberOfRemainingSubops = rsp.msg.CGetRSP.NumberOfRemainingSubOperations; + // getRSP->m_numberOfCompletedSubops = rsp.msg.CGetRSP.NumberOfCompletedSubOperations; + // getRSP->m_numberOfFailedSubops = rsp.msg.CGetRSP.NumberOfFailedSubOperations; + // getRSP->m_numberOfWarningSubops = rsp.msg.CGetRSP.NumberOfWarningSubOperations; + // getRSP->m_statusDetail = statusDetail; + + } + // 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 /*callback*/, NULL /*callbackData*/); // TODO-GET + + if (result.bad()) + { + desiredCStoreReturnStatus = STATUS_STORE_Error_CannotUnderstand; + // TODO-GET: return ? + } + 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 /* progress callback TODO-GET */, NULL /* callback context */, 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) : parameters_(params), association_(new DicomAssociation)
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h Tue Oct 29 17:25:49 2024 +0100 @@ -37,6 +37,14 @@ { 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 + ); + + class DicomControlUserConnection : public boost::noncopyable { private: @@ -72,6 +80,10 @@ 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/Enumerations.cpp Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancFramework/Sources/Enumerations.cpp Tue Oct 29 17:25:49 2024 +0100 @@ -369,6 +369,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";
--- a/OrthancFramework/Sources/Enumerations.h Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancFramework/Sources/Enumerations.h Tue Oct 29 17:25:49 2024 +0100 @@ -233,6 +233,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 };
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp Tue Oct 29 17:25:49 2024 +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 Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Tue Oct 29 17:25:49 2024 +0100 @@ -792,7 +792,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/OrthancServer/CMakeLists.txt Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancServer/CMakeLists.txt Tue Oct 29 17:25:49 2024 +0100 @@ -130,6 +130,7 @@ ${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/DicomGetScuJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomMoveScuJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/LuaJobManager.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/MergeStudyJob.cpp
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue Oct 29 17:25:49 2024 +0100 @@ -323,6 +323,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/Resources/DicomConformanceStatement.txt Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancServer/Resources/DicomConformanceStatement.txt Tue Oct 29 17:25:49 2024 +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/OrthancRestApi/OrthancRestModalities.cpp Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Tue Oct 29 17:25:49 2024 +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" @@ -1623,6 +1624,80 @@ } + /*************************************************************************** + * 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") + .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; + + if (!call.ParseJsonRequest(request) || + request.type() != Json::objectValue || + !request.isMember(KEY_QUERY) || + request[KEY_QUERY].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " + + std::string(KEY_QUERY)); + } + + std::string localAet = Toolbox::GetJsonStringField // TODO-GET: keep this ? + (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle()); + + const RemoteModalityParameters source = + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + std::unique_ptr<DicomGetScuJob> job(new DicomGetScuJob(context)); + + job->SetQueryFormat(DicomToJsonFormat_Short); // TODO-GET: keep this ? + job->SetLocalAet(localAet); + job->SetRemoteModality(source); + + // TODO-GET: asynchronous + // TODO-GET: permissive + + + if (request[KEY_QUERY].isMember("PatientID")) // TODO-GET: handle get of multiple resources + series + instances + { + job->AddResourceToRetrieve(ResourceType_Patient, request[KEY_QUERY]["PatientID"].asString()); + } + else if (request[KEY_QUERY].isMember("StudyInstanceUID")) // TODO-GET: handle get of multiple resources + series + instances + { + job->AddResourceToRetrieve(ResourceType_Study, request[KEY_QUERY]["StudyInstanceUID"].asString()); + } + + if (request.isMember(KEY_TIMEOUT)) + { + job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT)); + } + + OrthancRestApi::GetApi(call).SubmitCommandsJob + (call, job.release(), true /* synchronous by default */, request); + return; + } + /*************************************************************************** * Orthanc Peers => Store client @@ -2544,6 +2619,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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp Tue Oct 29 17:25:49 2024 +0100 @@ -0,0 +1,276 @@ +/** + * 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> + +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 +{ + class DicomGetScuJob::Command : public SetOfCommandsJob::ICommand + { + private: + DicomGetScuJob& that_; + std::unique_ptr<DicomMap> findAnswer_; + + public: + Command(DicomGetScuJob& 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 DicomGetScuJob::Unserializer : + public SetOfCommandsJob::ICommandUnserializer + { + private: + DicomGetScuJob& that_; + + public: + explicit Unserializer(DicomGetScuJob& that) : + that_(that) + { + } + + virtual ICommand* Unserialize(const Json::Value& source) const ORTHANC_OVERRIDE + { + DicomMap findAnswer; + findAnswer.Unserialize(source); + return new Command(that_, findAnswer); + } + }; + + + 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) + { + connection_.reset(new DicomControlUserConnection(parameters_)); + } + + connection_->Get(findAnswer, InstanceReceivedHandler, &context_); + } + + void DicomGetScuJob::AddResourceToRetrieve(ResourceType level, const std::string& dicomId) + { + // TODO-GET: when retrieving a single series, one must provide the StudyInstanceUID too + DicomMap item; + + switch (level) + { + case ResourceType_Patient: + item.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, ResourceTypeToDicomQueryRetrieveLevel(level), false); + item.SetValue(DICOM_TAG_PATIENT_ID, dicomId, false); + break; + + case ResourceType_Study: + item.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, ResourceTypeToDicomQueryRetrieveLevel(level), false); + item.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, dicomId, false); + break; + + case ResourceType_Series: + item.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, ResourceTypeToDicomQueryRetrieveLevel(level), false); + item.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, dicomId, false); + break; + + case ResourceType_Instance: + item.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, ResourceTypeToDicomQueryRetrieveLevel(level), false); + item.SetValue(DICOM_TAG_SOP_INSTANCE_UID, dicomId, false); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + query_.Add(item); + + AddCommand(new Command(*this, item)); + } + + void DicomGetScuJob::SetLocalAet(const std::string& aet) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetLocalApplicationEntityTitle(aet); + } + } + + + void DicomGetScuJob::SetRemoteModality(const RemoteModalityParameters& remote) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetRemoteModality(remote); + } + } + + + void DicomGetScuJob::SetTimeout(uint32_t seconds) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetTimeout(seconds); + } + } + + + void DicomGetScuJob::Stop(JobStopReason reason) + { + connection_.reset(); + } + + + void DicomGetScuJob::SetQueryFormat(DicomToJsonFormat format) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + queryFormat_ = format; + } + } + + + void DicomGetScuJob::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_); + } + + + DicomGetScuJob::DicomGetScuJob(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) + { + 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 DicomGetScuJob::Serialize(Json::Value& target) + { + if (!SetOfCommandsJob::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; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.h Tue Oct 29 17:25:49 2024 +0100 @@ -0,0 +1,100 @@ +/** + * 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 "../QueryRetrieveHandler.h" + +namespace Orthanc +{ + class ServerContext; + + class DicomGetScuJob : public SetOfCommandsJob + { + private: + class Command; + class Unserializer; + + ServerContext& context_; + DicomAssociationParameters parameters_; + DicomFindAnswers query_; + DicomToJsonFormat queryFormat_; // New in 1.9.5 + + std::unique_ptr<DicomControlUserConnection> connection_; + + void Retrieve(const DicomMap& findAnswer); + + public: + explicit DicomGetScuJob(ServerContext& context) : + context_(context), + query_(false /* this is not for worklists */), + queryFormat_(DicomToJsonFormat_Short) + { + } + + DicomGetScuJob(ServerContext& context, + const Json::Value& serialized); + + // void AddFindAnswer(const DicomMap& answer); + + // void AddQuery(const DicomMap& query); + + // void AddFindAnswer(QueryRetrieveHandler& query, + // size_t i); + + void AddResourceToRetrieve(ResourceType level, const std::string& dicomId); + + 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 GetJobType(std::string& target) ORTHANC_OVERRIDE + { + target = "DicomGetScu"; + } + + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; + + virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; + }; +}
--- a/OrthancServer/Sources/main.cpp Wed Oct 16 19:34:42 2024 +0200 +++ b/OrthancServer/Sources/main.cpp Tue Oct 29 17:25:49 2024 +0100 @@ -886,6 +886,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"); }
--- a/TODO Wed Oct 16 19:34:42 2024 +0200 +++ b/TODO Tue Oct 29 17:25:49 2024 +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 === =======================