# HG changeset patch # User Alain Mazy # Date 1732187867 -3600 # Node ID 7f4fab033c87f54fae885078bd0b19cee97f44e2 # Parent 94e6a9a66109c2ce6be4058a0866bebba8df9511 RejectedSopClasses + /queries/../get to retrieve a C-FIND answer diff -r 94e6a9a66109 -r 7f4fab033c87 NEWS --- a/NEWS Wed Nov 20 09:58:12 2024 +0100 +++ b/NEWS Thu Nov 21 12:17:47 2024 +0100 @@ -6,11 +6,12 @@ * DICOM: - Added support for C-GET SCU. - - Added a configuration "AcceptedSopClasses" to limit the SOP classes accepted by - Orthanc. + - Added a configuration "AcceptedSopClasses" and "RejectedSopClasses" to limit + the SOP classes accepted by Orthanc when acting as C-STORE SCP. + REST API ------------ +-------- * API version upgraded to 26 * Improved parsing of multiple numerical values in DICOM tags. diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancFramework/Sources/DicomFormat/DicomMap.cpp --- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Thu Nov 21 12:17:47 2024 +0100 @@ -1300,7 +1300,22 @@ return value->CopyToString(result, allowBinary); } } - + + bool DicomMap::LookupStringValues(std::set& 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 { diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancFramework/Sources/DicomFormat/DicomMap.h --- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Thu Nov 21 12:17:47 2024 +0100 @@ -179,7 +179,11 @@ bool LookupStringValue(std::string& result, const DicomTag& tag, bool allowBinary) const; - + + bool LookupStringValues(std::set& results, + const DicomTag& tag, + bool allowBinary) const; + bool ParseInteger32(int32_t& result, const DicomTag& tag) const; diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancFramework/Sources/Toolbox.h --- a/OrthancFramework/Sources/Toolbox.h Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancFramework/Sources/Toolbox.h Thu Nov 21 12:17:47 2024 +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 static void GetIntersection(std::set& target, const std::set& a, const std::set& b) { target.clear(); diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/CMakeLists.txt Thu Nov 21 12:17:47 2024 +0100 @@ -176,6 +176,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 diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp --- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp Thu Nov 21 12:17:47 2024 +0100 @@ -32,7 +32,8 @@ #include /* for variable dcmAllStorageSOPClassUIDs */ DicomFilter::DicomFilter() : - hasAcceptedTransferSyntaxes_(false) + hasAcceptedTransferSyntaxes_(false), + hasAcceptedStorageClasses_(false) { { OrthancPlugins::OrthancConfiguration config; @@ -41,7 +42,6 @@ alwaysAllowMove_ = config.GetBooleanValue("DicomAlwaysAllowMove", false); alwaysAllowStore_ = config.GetBooleanValue("DicomAlwaysAllowStore", true); unknownSopClassAccepted_ = config.GetBooleanValue("UnknownSopClassAccepted", false); - config.LookupSetOfStrings(acceptedStorageClasses_, "AcceptedSopClasses", false); isStrict_ = config.GetBooleanValue("StrictAetComparison", false); checkModalityHost_ = config.GetBooleanValue("DicomCheckModalityHost", false); } @@ -212,40 +212,43 @@ void DicomFilter::GetAcceptedSopClasses(std::set& sopClasses, size_t maxCount) { - boost::shared_lock lock(mutex_); - - if (acceptedStorageClasses_.size() >= 0) + boost::unique_lock lock(mutex_); + + if (!hasAcceptedStorageClasses_) { - size_t count = 0; - std::set::const_iterator it = acceptedStorageClasses_.begin(); + Json::Value jsonSopClasses; - while (it != acceptedStorageClasses_.end() && (maxCount == 0 || count < maxCount)) + if (!OrthancPlugins::RestApiGet(jsonSopClasses, "/tools/accepted-sop-classes", false) || + jsonSopClasses.type() != Json::arrayValue) { - sopClasses.insert(*it); - count++; - } - } - else - { - if (maxCount != 0) - { - size_t count = 0; - // we actually take a list of default 120 most common storage SOP classes defined in DCMTK - while (dcmLongSCUStorageSOPClassUIDs[count] != NULL && count < maxCount) - { - sopClasses.insert(dcmAllStorageSOPClassUIDs[count]); - count++; - } + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } else { - size_t count = 0; - // we actually take all known storage SOP classes defined in DCMTK - while (dcmAllStorageSOPClassUIDs[count] != NULL) + for (Json::Value::ArrayIndex i = 0; i < jsonSopClasses.size(); i++) { - sopClasses.insert(dcmAllStorageSOPClassUIDs[count]); - count++; + if (jsonSopClasses[i].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + acceptedStorageClasses_.insert(jsonSopClasses[i].asString()); + } } } + + hasAcceptedStorageClasses_ = true; } + + std::set::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 diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h --- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h Thu Nov 21 12:17:47 2024 +0100 @@ -44,6 +44,7 @@ bool hasAcceptedTransferSyntaxes_; std::set acceptedTransferSyntaxes_; + bool hasAcceptedStorageClasses_; std::set acceptedStorageClasses_; public: diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/Resources/Configuration.json --- a/OrthancServer/Resources/Configuration.json Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/Resources/Configuration.json Thu Nov 21 12:17:47 2024 +0100 @@ -207,12 +207,26 @@ // to all DCMTK storage classes in case of C-STORE SCP // and to a reduced list of 120 most standard 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 adda 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 has the default value. + // 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 diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Thu Nov 21 12:17:47 2024 +0100 @@ -914,6 +914,59 @@ } + static void SubmitGetScuJob(RestApiPostCall& call, + bool allAnswers, + size_t index) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + int timeout = -1; + Json::Value body; + + if (call.ParseJsonRequest(body)) + { + timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1); + } + + std::unique_ptr job(new DicomGetScuJob(context)); + job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short)); + + { + QueryAccessor query(call); + job->SetRemoteModality(query.GetHandler().GetRemoteModality()); + + if (timeout >= 0) + { + // New in Orthanc 1.7.0 + job->SetTimeout(static_cast(timeout)); + } + else if (query.GetHandler().HasTimeout()) + { + // New in Orthanc 1.9.1 + job->SetTimeout(query.GetHandler().GetTimeout()); + } + + LOG(WARNING) << "Driving C-Get SCU on remote modality " + << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle(); + + if (allAnswers) + { + for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++) + { + job->AddFindAnswer(query.GetHandler(), i); + } + } + else + { + job->AddFindAnswer(query.GetHandler(), index); + } + } + + OrthancRestApi::GetApi(call).SubmitCommandsJob + (call, job.release(), true /* synchronous by default */, body); + } + + static void SubmitRetrieveJob(RestApiPostCall& call, bool allAnswers, size_t index) @@ -1007,7 +1060,7 @@ { DocumentRetrieveShared(call); call.GetDocumentation() - .SetSummary("Retrieve one answer") + .SetSummary("Retrieve one answer with a C-MOVE SCU") .SetDescription("Start a C-MOVE 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") @@ -1020,13 +1073,49 @@ } + static void RetrieveOneAnswerWithGet(RestApiPostCall& call) + { + if (call.IsDocumentation()) + { + DocumentRetrieveShared(call); + call.GetDocumentation() + .SetSummary("Retrieve one answer with a C-GET SCU") + .SetDescription("Start 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-get") // TODO-GET: write doc + .SetUriArgument("index", "Index of the answer"); + return; + } + + size_t index = boost::lexical_cast(call.GetUriComponent("index", "")); + SubmitRetrieveJob(call, false, index); + } + + + static void RetrieveAllAnswersWithGet(RestApiPostCall& call) + { + if (call.IsDocumentation()) + { + DocumentRetrieveShared(call); + call.GetDocumentation() + .SetSummary("Retrieve all answers with C-GET SCU") + .SetDescription("Start a C-GET 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-get"); + return; + } + + SubmitGetScuJob(call, true, 0); + } + + 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"); @@ -2638,6 +2727,7 @@ Register("/queries/{id}/answers/{index}", ListQueryAnswerOperations); Register("/queries/{id}/answers/{index}/content", GetQueryOneAnswer); Register("/queries/{id}/answers/{index}/retrieve", RetrieveOneAnswer); + Register("/queries/{id}/answers/{index}/get", RetrieveOneAnswerWithGet); Register("/queries/{id}/answers/{index}/query-instances", QueryAnswerChildren); Register("/queries/{id}/answers/{index}/query-series", @@ -2648,6 +2738,7 @@ Register("/queries/{id}/modality", GetQueryModality); Register("/queries/{id}/query", GetQueryArguments); Register("/queries/{id}/retrieve", RetrieveAllAnswers); + Register("/queries/{id}/get", RetrieveAllAnswersWithGet); Register("/peers", ListPeers); Register("/peers/{id}", ListPeerOperations); diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Thu Nov 21 12:17:47 2024 +0100 @@ -466,6 +466,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 sopClasses; + OrthancRestApi::GetContext(call).GetAcceptedSopClasses(sopClasses, 0); + + Json::Value json = Json::arrayValue; + for (std::set::const_iterator + sop = sopClasses.begin(); sop != sopClasses.end(); ++sop) + { + json.append(*sop); + } + + call.GetOutput().AnswerJson(json); + } + static void GetUnknownSopClassAccepted(RestApiGetCall& call) { @@ -1188,5 +1215,8 @@ Register("/tools/unknown-sop-class-accepted", SetUnknownSopClassAccepted); Register("/tools/labels", ListAllLabels); // New in Orthanc 1.12.0 + + Register("/tools/accepted-sop-classes", GetAcceptedSopClasses); + } } diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Thu Nov 21 12:17:47 2024 +0100 @@ -51,6 +51,8 @@ #include #include /* for variable dcmAllStorageSOPClassUIDs */ +#include + #if HAVE_MALLOC_TRIM == 1 # include #endif @@ -487,7 +489,11 @@ isUnknownSopClassAccepted_ = lock.GetConfiguration().GetBooleanParameter("UnknownSopClassAccepted", false); - lock.GetConfiguration().GetSetOfStringsParameter(acceptedSopClasses_, "AcceptedSopClasses"); + std::list acceptedSopClasses; + std::set rejectedSopClasses; + lock.GetConfiguration().GetListOfStringsParameter(acceptedSopClasses, "AcceptedSopClasses"); + lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, "RejectSopClasses"); + SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses); } jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); @@ -2110,49 +2116,114 @@ } } - void ServerContext::SetAcceptedSopClasses(const std::set& sopClasses) + void ServerContext::SetAcceptedSopClasses(const std::list& acceptedSopClasses, + const std::set& rejectedSopClasses) { boost::mutex::scoped_lock lock(dynamicOptionsMutex_); - acceptedSopClasses_ = sopClasses; + acceptedSopClasses_.clear(); + + size_t count = 0; + std::set allDcmtkSopClassUids; + std::set 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::const_iterator it = shortDcmtkSopClassUids.begin(); it != shortDcmtkSopClassUids.end(); ++it) + { + acceptedSopClasses_.push_back(*it); + } + + for (std::set::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 addedSopClasses; + + for (std::list::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::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::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::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& sopClasses, size_t maxCount) const { + sopClasses.clear(); + boost::mutex::scoped_lock lock(dynamicOptionsMutex_); - if (acceptedSopClasses_.size() > 0) - { - size_t count = 0; - std::set::const_iterator it = acceptedSopClasses_.begin(); - - while (it != acceptedSopClasses_.end() && (maxCount == 0 || count < maxCount)) - { - sopClasses.insert(*it); - count++; - } - } - else + size_t count = 0; + std::list::const_iterator it = acceptedSopClasses_.begin(); + + while (it != acceptedSopClasses_.end() && (maxCount == 0 || count < maxCount)) { - if (maxCount != 0) - { - size_t count = 0; - // we actually take a list of default 120 most common storage SOP classes defined in DCMTK - while (dcmLongSCUStorageSOPClassUIDs[count] != NULL && count < maxCount) - { - sopClasses.insert(dcmAllStorageSOPClassUIDs[count]); - count++; - } - } - else - { - size_t count = 0; - // we actually take all known storage SOP classes defined in DCMTK - while (dcmAllStorageSOPClassUIDs[count] != NULL) - { - sopClasses.insert(dcmAllStorageSOPClassUIDs[count]); - count++; - } - } + sopClasses.insert(*it); + count++; + it++; } } diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.h Thu Nov 21 12:17:47 2024 +0100 @@ -277,7 +277,7 @@ mutable boost::mutex dynamicOptionsMutex_; bool isUnknownSopClassAccepted_; std::set acceptedTransferSyntaxes_; - std::set acceptedSopClasses_; + std::list acceptedSopClasses_; // ordered; the most 120 common ones first StoreResult StoreAfterTranscoding(std::string& resultPublicId, DicomInstanceToStore& dicom, @@ -597,7 +597,8 @@ void SetAcceptedTransferSyntaxes(const std::set& syntaxes); - void SetAcceptedSopClasses(const std::set& sopClasses); + void SetAcceptedSopClasses(const std::list& acceptedSopClasses, + const std::set& rejectedSopClasses); void GetAcceptedSopClasses(std::set& sopClasses, size_t maxCount) const; diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp --- a/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp Thu Nov 21 12:17:47 2024 +0100 @@ -27,6 +27,7 @@ #include "../../../OrthancFramework/Sources/SerializationToolbox.h" #include "../ServerContext.h" #include +#include static const char* const LOCAL_AET = "LocalAet"; static const char* const QUERY = "Query"; @@ -112,32 +113,68 @@ { if (connection_.get() == NULL) { - std::set storageSopClassUids; + std::set sopClassesToPropose; + std::set sopClassesInStudy; + std::set acceptedSopClasses; std::set storageAcceptedTransferSyntaxes; - if (findAnswer.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) + if (findAnswer.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) && + findAnswer.LookupStringValues(sopClassesInStudy, DICOM_TAG_SOP_CLASSES_IN_STUDY, false)) { - throw OrthancException(ErrorCode_NotImplemented); - // TODO-GET + context_.GetAcceptedSopClasses(acceptedSopClasses, 0); + + // keep the sop classes from the resources to retrieve only if they are accepted by Orthanc + Toolbox::GetIntersection(sopClassesToPropose, sopClassesInStudy, acceptedSopClasses); } else { - // when we don't know what SOP Classes to use, we must limit to 120 SOP Classes because + // 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(storageSopClassUids, 120); + context_.GetAcceptedSopClasses(sopClassesToPropose, 120); + } + + if (sopClassesToPropose.size() == 0) + { + throw OrthancException(ErrorCode_NoPresentationContext, "Cannot perform C-Get, no SOPClassUID have been accepted by Orthanc."); } context_.GetAcceptedTransferSyntaxes(storageAcceptedTransferSyntaxes); connection_.reset(new DicomControlUserConnection(parameters_, ScuOperationFlags_Get, - storageSopClassUids, + sopClassesToPropose, storageAcceptedTransferSyntaxes)); } connection_->Get(findAnswer, InstanceReceivedHandler, &context_); } + // 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 DicomGetScuJob::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); + + query_.Add(item); + query_.GetAnswer(query_.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); // Remove the "SpecificCharacterSet" (0008,0005) tag that is automatically added if creating a ParsedDicomFile object from a DicomMap + + AddCommand(new Command(*this, answer)); + } + + void DicomGetScuJob::AddFindAnswer(QueryRetrieveHandler& query, + size_t i) + { + DicomMap answer; + query.GetAnswer(answer, i); + AddFindAnswer(answer); + } + void DicomGetScuJob::AddResourceToRetrieve(ResourceType level, const std::string& dicomId) { // TODO-GET: when retrieving a single series, one must provide the StudyInstanceUID too @@ -218,7 +255,7 @@ } - void DicomGetScuJob::SetQueryFormat(DicomToJsonFormat format) + void DicomGetScuJob::SetQueryFormat(DicomToJsonFormat format) // TODO-GET: is this usefull ? { if (IsStarted()) { diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/Sources/ServerJobs/DicomGetScuJob.h --- a/OrthancServer/Sources/ServerJobs/DicomGetScuJob.h Wed Nov 20 09:58:12 2024 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.h Thu Nov 21 12:17:47 2024 +0100 @@ -59,12 +59,12 @@ DicomGetScuJob(ServerContext& context, const Json::Value& serialized); - // void AddFindAnswer(const DicomMap& answer); + void AddFindAnswer(const DicomMap& answer); // void AddQuery(const DicomMap& query); - // void AddFindAnswer(QueryRetrieveHandler& query, - // size_t i); + void AddFindAnswer(QueryRetrieveHandler& query, + size_t i); void AddResourceToRetrieve(ResourceType level, const std::string& dicomId); diff -r 94e6a9a66109 -r 7f4fab033c87 OrthancServer/UnitTestsSources/ServerConfigTests.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Thu Nov 21 12:17:47 2024 +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 . + **/ + + +#include "PrecompiledHeadersUnitTests.h" +#include + +#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); + + { // default config -> all SOP Classes should be accepted + std::set 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 acceptedStorageClasses; + std::set rejectedStorageClasses; + + std::set 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(); +}