# HG changeset patch # User Alain Mazy # Date 1763483609 -3600 # Node ID bd0fb2559a7ec959b006526caaf10cdeedd3e975 # Parent 96676e815c1f8e412165558e0bdbb90602389a8a# Parent 8b2ef7afec935ad5a6f833da7c11ce3e4523b717 merge diff -r 8b2ef7afec93 -r bd0fb2559a7e NEWS --- a/NEWS Tue Nov 18 17:21:29 2025 +0100 +++ b/NEWS Tue Nov 18 17:33:29 2025 +0100 @@ -51,29 +51,6 @@ - civetweb 1.16, including patch for CVE-2025-55763 - SQLite 3.50.4 -Plugins -------- - -* Worklists plugin: - - The Worklists plugin now provides a REST API to: - - create worklists through POST at /worklists/create - - list the worklists through GET at /worklists - - view a single worklist through GET at /worklists/{uuid} - - modify a worklist through PUT at /worklists/{uuid} - - delete a worklist through DELETE at /worklists/{uuid} - All details are available in the Orthanc book: - https://orthanc.uclouvain.be/book/plugins/worklists-plugin.html - - New configuration options: - - "SaveInOrthancDatabase" to store the worklists in the Orthanc DB (provided that you are using SQLite or PostgreSQL). - - "DeleteWorklistsOnStableStudy" to delete the worklist once its related study has been received and is stable. - - "SetStudyInstanceUidIfMissing" to add a StudyInstanceUID if the REST API request does not include one. - - "DeleteWorklistsDelay" to delete a worklist N hours after it has been created - (only available if using the "SaveInOrthancDatabase" mode). - - "HousekeepingInterval" to define the delay between 2 executions of the Worklist Housekeeping thread - that deletes the worklists when required. - - Note: the previous "Database" configuration has now been renamed in "Directory" to better differentiate - the "File" or "DB modes. - Version 1.12.9 (2025-08-11) =========================== diff -r 8b2ef7afec93 -r bd0fb2559a7e OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Tue Nov 18 17:21:29 2025 +0100 +++ b/OrthancServer/CMakeLists.txt Tue Nov 18 17:33:29 2025 +0100 @@ -636,14 +636,6 @@ list(APPEND MODALITY_WORKLISTS_RESOURCES ${AUTOGENERATED_DIR}/ModalityWorklists.rc) endif() - EmbedResources( - --target=ModalityWorklistsResources - --namespace=Orthanc.FrameworkResources - --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources - ${LIBICU_RESOURCES} - ${DCMTK_DICTIONARIES} - ) - set_source_files_properties( ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp PROPERTIES COMPILE_DEFINITIONS "MODALITY_WORKLISTS_VERSION=\"${ORTHANC_VERSION}\"" @@ -651,14 +643,12 @@ add_library(ModalityWorklists SHARED ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp - ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/OrthancFrameworkDependencies.cpp - ${AUTOGENERATED_DIR}/ModalityWorklistsResources.cpp ${MODALITY_WORKLISTS_RESOURCES} ) DefineSourceBasenameForTarget(ModalityWorklists) - target_link_libraries(ModalityWorklists PluginsDependencies ${DCMTK_LIBRARIES}) + target_link_libraries(ModalityWorklists PluginsDependencies) set_target_properties( ModalityWorklists PROPERTIES diff -r 8b2ef7afec93 -r bd0fb2559a7e OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt --- a/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt Tue Nov 18 17:21:29 2025 +0100 +++ b/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt Tue Nov 18 17:33:29 2025 +0100 @@ -27,31 +27,22 @@ SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) +SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") +SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") -set(ENABLE_JPEG ON CACHE INTERNAL "") -set(ENABLE_PNG ON CACHE INTERNAL "") -set(ENABLE_ZLIB ON CACHE INTERNAL "") -set(ENABLE_LOCALE ON CACHE INTERNAL "") -set(ENABLE_DCMTK ON CACHE INTERNAL "") -set(ENABLE_MODULE_DICOM ON CACHE INTERNAL "") -set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "") - -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) +include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake) +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/BoostConfiguration.cmake) add_library(ModalityWorklists SHARED Plugin.cpp ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp - ${ORTHANC_CORE_SOURCES} - ${ORTHANC_DICOM_SOURCES} - ${AUTOGENERATED_SOURCES} + ${JSONCPP_SOURCES} + ${BOOST_SOURCES} ) DefineSourceBasenameForTarget(ModalityWorklists) -target_link_libraries(ModalityWorklists ${DCMTK_LIBRARIES}) - message("Setting the version of the plugin to ${MODALITY_WORKLISTS_VERSION}") add_definitions( -DMODALITY_WORKLISTS_VERSION="${MODALITY_WORKLISTS_VERSION}" diff -r 8b2ef7afec93 -r bd0fb2559a7e OrthancServer/Plugins/Samples/ModalityWorklists/OrthancFrameworkDependencies.cpp --- a/OrthancServer/Plugins/Samples/ModalityWorklists/OrthancFrameworkDependencies.cpp Tue Nov 18 17:21:29 2025 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2025 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 . - **/ - - -/** - * Remove the dependency upon ICU in plugins, as this greatly increase - * the size of the resulting binaries, since they must embed the ICU - * dictionary. - **/ - -#if BOOST_LOCALE_WITH_ICU == 1 -# undef BOOST_LOCALE_WITH_ICU -# if ORTHANC_STATIC_ICU == 1 -# include - -// Define an empty ICU dictionary for static builds -extern "C" -{ - struct - { - double bogus; - uint8_t *bytes; - } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; -} - -# endif -#endif - -#include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp" -#include "../../../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp" -#include "../../../../OrthancFramework/Sources/Compression/GzipCompressor.cpp" -#include "../../../../OrthancFramework/Sources/Compression/ZlibCompressor.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomElement.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomPath.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomTag.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomValue.cpp" -#include "../../../../OrthancFramework/Sources/DicomFormat/Window.cpp" -#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp" -#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp" -#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp" -#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp" -#include "../../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp" -#include "../../../../OrthancFramework/Sources/Enumerations.cpp" -#include "../../../../OrthancFramework/Sources/HttpServer/HttpOutput.cpp" -#include "../../../../OrthancFramework/Sources/Images/IImageWriter.cpp" -#include "../../../../OrthancFramework/Sources/Images/Image.cpp" -#include "../../../../OrthancFramework/Sources/Images/ImageAccessor.cpp" -#include "../../../../OrthancFramework/Sources/Images/ImageBuffer.cpp" -#include "../../../../OrthancFramework/Sources/Images/ImageProcessing.cpp" -#include "../../../../OrthancFramework/Sources/Images/JpegErrorManager.cpp" -#include "../../../../OrthancFramework/Sources/Images/JpegReader.cpp" -#include "../../../../OrthancFramework/Sources/Images/JpegWriter.cpp" -#include "../../../../OrthancFramework/Sources/Images/PamReader.cpp" -#include "../../../../OrthancFramework/Sources/Images/PamWriter.cpp" -#include "../../../../OrthancFramework/Sources/Images/PngReader.cpp" -#include "../../../../OrthancFramework/Sources/Images/PngWriter.cpp" -#include "../../../../OrthancFramework/Sources/Logging.cpp" -#include "../../../../OrthancFramework/Sources/MetricsRegistry.cpp" -#include "../../../../OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp" -#include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp" -#include "../../../../OrthancFramework/Sources/OrthancException.cpp" -#include "../../../../OrthancFramework/Sources/OrthancFramework.cpp" -#include "../../../../OrthancFramework/Sources/RestApi/RestApiOutput.cpp" -#include "../../../../OrthancFramework/Sources/SerializationToolbox.cpp" -#include "../../../../OrthancFramework/Sources/SystemToolbox.cpp" -#include "../../../../OrthancFramework/Sources/TemporaryFile.cpp" -#include "../../../../OrthancFramework/Sources/Toolbox.cpp" - -namespace Orthanc -{ - void HttpClient::GlobalInitialize() - { - } - - void HttpClient::GlobalFinalize() - { - } -} diff -r 8b2ef7afec93 -r bd0fb2559a7e OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp --- a/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp Tue Nov 18 17:21:29 2025 +0100 +++ b/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp Tue Nov 18 17:33:29 2025 +0100 @@ -24,84 +24,17 @@ #define MODALITY_WORKLISTS_NAME "worklists" #include "../../../../OrthancFramework/Sources/Compatibility.h" -#include "../../../../OrthancFramework/Sources/OrthancException.h" -#include "../../../../OrthancFramework/Sources/Logging.h" -#include "../../../../OrthancFramework/Sources/Toolbox.h" -#include "../../../../OrthancFramework/Sources/SystemToolbox.h" -#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomPath.h" -#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" -#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h" #include "../Common/OrthancPluginCppWrapper.h" -#include #include #include #include #include #include -static boost::filesystem::path worklistDirectory_; +static std::string folder_; static bool filterIssuerAet_ = false; static unsigned int limitAnswers_ = 0; -static std::unique_ptr worklistHousekeeperThread_; -static bool worklistHousekeeperThreadShouldStop_ = false; -static bool deleteWorklistsOnStableStudy_ = true; -static unsigned int hkIntervalInSeconds_ = 60; -static unsigned int deleteDelayInHours_ = 24; -static std::unique_ptr worklistsStore_; -static bool setStudyInstanceUidIfMissing_ = true; - -enum WorklistStorageType -{ - WorklistStorageType_Folder = 1, - WorklistStorageType_OrthancDb = 2 -}; - -static WorklistStorageType worklistStorage_ = WorklistStorageType_Folder; - -struct Worklist -{ - std::string id_; - std::string createdAt_; - std::string dicomContent_; - - Worklist(const std::string& id, - const std::string& dicomContent) : - id_(id), - createdAt_(Orthanc::SystemToolbox::GetNowIsoString(true)), - dicomContent_(dicomContent) - { - } - - - Worklist(const std::string& id, const Json::Value& jsonWl) : - id_(id), - createdAt_(jsonWl["CreatedAt"].asString()) - { - std::string b64DicomContent = jsonWl["Dicom"].asString(); - Orthanc::Toolbox::DecodeBase64(dicomContent_, b64DicomContent); - } - - void Serialize(std::string& target) const - { - Json::Value t; - t["CreatedAt"] = createdAt_; - std::string b64DicomContent; - Orthanc::Toolbox::EncodeBase64(b64DicomContent, dicomContent_); - t["Dicom"] = b64DicomContent; - - Orthanc::Toolbox::WriteFastJson(target, t); - } - - bool IsOlderThan(unsigned int delayInHours) const - { - boost::posix_time::ptime now(boost::posix_time::from_iso_string(Orthanc::SystemToolbox::GetNowIsoString(true))); - boost::posix_time::ptime creationDate(boost::posix_time::from_iso_string(createdAt_)); - - return (now - creationDate).total_seconds() > (3600 * deleteDelayInHours_); - } -}; /** * This is the main function for matching a DICOM worklist against a query. @@ -109,10 +42,10 @@ static bool MatchWorklist(OrthancPluginWorklistAnswers* answers, const OrthancPluginWorklistQuery* query, const OrthancPlugins::FindMatcher& matcher, - const std::string& dicomContent) + const std::string& path) { OrthancPlugins::MemoryBuffer dicom; - dicom.Assign(dicomContent); + dicom.ReadFile(path); if (matcher.IsMatch(dicom)) { @@ -204,116 +137,70 @@ } } -static void ListWorklistsFromDb(std::vector& target) -{ - assert(worklistStorage_ == WorklistStorageType_OrthancDb); - assert(worklistsStore_); - - std::unique_ptr it(worklistsStore_->CreateIterator()); - - while (it->Next()) - { - std::string serialized; - it->GetValue(serialized); - - Json::Value jsonWl; - if (Orthanc::Toolbox::ReadJson(jsonWl, serialized)) - { - Worklist wl(it->GetKey(), jsonWl); - target.push_back(wl); - } - }; - -} -static void ListWorklistsFromFolder(std::vector& target) -{ - assert(worklistStorage_ == WorklistStorageType_Folder); - - // Loop over the regular files in the database folder - namespace fs = boost::filesystem; - - fs::path source = worklistDirectory_; - fs::directory_iterator end; - - try - { - for (fs::directory_iterator it(source); it != end; ++it) - { - fs::file_type type(it->status().type()); - - if (type == fs::regular_file || - type == fs::reparse_file) // cf. BitBucket issue #11 - { - std::string extension = it->path().extension().string(); - std::transform(extension.begin(), extension.end(), extension.begin(), tolower); // Convert to lowercase - - if (extension == ".wl") - { - std::string worklistId = Orthanc::SystemToolbox::PathToUtf8(it->path().filename().replace_extension("")); - std::string dicomContent; - Orthanc::SystemToolbox::ReadFile(dicomContent, it->path()); - - Worklist wl(worklistId, dicomContent); - target.push_back(wl); - } - } - } - } - catch (fs::filesystem_error&) - { - LOG(ERROR) << "Inexistent folder while scanning for worklists: " + source.string(); - } -} - - -OrthancPluginErrorCode WorklistCallback(OrthancPluginWorklistAnswers* answers, - const OrthancPluginWorklistQuery* query, - const char* issuerAet, - const char* calledAet) +OrthancPluginErrorCode Callback(OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const char* issuerAet, + const char* calledAet) { try { - unsigned int parsedFilesCount = 0; - unsigned int matchedWorklistCount = 0; - // Construct an object to match the worklists in the database against the C-Find query std::unique_ptr matcher(CreateMatcher(query, issuerAet)); - std::vector worklists; + // Loop over the regular files in the database folder + namespace fs = boost::filesystem; - if (worklistStorage_ == WorklistStorageType_Folder) + fs::path source(folder_); + fs::directory_iterator end; + + try { - ListWorklistsFromFolder(worklists); - } - else if (worklistStorage_ == WorklistStorageType_OrthancDb) - { - ListWorklistsFromDb(worklists); - } - - for (std::vector::const_iterator it = worklists.begin(); it != worklists.end(); ++it) - { - if (MatchWorklist(answers, query, *matcher, it->dicomContent_)) + unsigned int parsedFilesCount = 0; + unsigned int matchedWorklistCount = 0; + + for (fs::directory_iterator it(source); it != end; ++it) { - if (limitAnswers_ != 0 && - matchedWorklistCount >= limitAnswers_) + fs::file_type type(it->status().type()); + + if (type == fs::regular_file || + type == fs::reparse_file) // cf. BitBucket issue #11 { - // Too many answers are to be returned wrt. the - // "LimitAnswers" configuration parameter. Mark the - // C-FIND result as incomplete. - OrthancPluginWorklistMarkIncomplete(OrthancPlugins::GetGlobalContext(), answers); - return OrthancPluginErrorCode_Success; + std::string extension = it->path().extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), tolower); // Convert to lowercase + + if (extension == ".wl") + { + parsedFilesCount++; + // We found a worklist (i.e. a DICOM find with extension ".wl"), match it against the query + if (MatchWorklist(answers, query, *matcher, it->path().string())) + { + if (limitAnswers_ != 0 && + matchedWorklistCount >= limitAnswers_) + { + // Too many answers are to be returned wrt. the + // "LimitAnswers" configuration parameter. Mark the + // C-FIND result as incomplete. + OrthancPluginWorklistMarkIncomplete(OrthancPlugins::GetGlobalContext(), answers); + return OrthancPluginErrorCode_Success; + } + + ORTHANC_PLUGINS_LOG_INFO("Worklist matched: " + it->path().string()); + matchedWorklistCount++; + } + } } - - LOG(INFO) << "Worklist matched: " << it->id_; - matchedWorklistCount++; } + + ORTHANC_PLUGINS_LOG_INFO("Worklist C-Find: parsed " + boost::lexical_cast(parsedFilesCount) + + " files, found " + boost::lexical_cast(matchedWorklistCount) + " match(es)"); } - - LOG(INFO) << "Worklist C-Find: parsed " << boost::lexical_cast(parsedFilesCount) << - " worklists, found " << boost::lexical_cast(matchedWorklistCount) << " match(es)"; - + catch (fs::filesystem_error&) + { + ORTHANC_PLUGINS_LOG_ERROR("Inexistent folder while scanning for worklists: " + source.string()); + return OrthancPluginErrorCode_DirectoryExpected; + } return OrthancPluginErrorCode_Success; } @@ -323,411 +210,13 @@ } } -static void DeleteWorklist(const std::string& worklistId) -{ - switch (worklistStorage_) - { - case WorklistStorageType_Folder: - { - boost::filesystem::path path = worklistDirectory_ / (worklistId + ".wl"); - if (!Orthanc::SystemToolbox::IsRegularFile(path)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); - } - Orthanc::SystemToolbox::RemoveFile(path); - break; - } - - case WorklistStorageType_OrthancDb: - if (worklistsStore_.get() != NULL) - { - std::string notUsed; - if (!worklistsStore_->GetValue(notUsed, worklistId)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); - } - worklistsStore_->DeleteKey(worklistId); - } - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - -} - -static void WorklistHkWorkerThread() -{ - OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "WL HOUSEKEEPER"); - - OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Starting Worklist Housekeeper worker thread"); - Orthanc::Toolbox::ElapsedTimer timer; - - while (!worklistHousekeeperThreadShouldStop_) - { - boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); - - if (timer.GetElapsedMilliseconds() > (hkIntervalInSeconds_ * 1000)) - { - timer.Restart(); - LOG(INFO) << "Performing Worklist Housekeeping"; - - std::vector worklists; - - if (worklistStorage_ == WorklistStorageType_OrthancDb) - { - ListWorklistsFromDb(worklists); - } - else if (worklistStorage_ == WorklistStorageType_Folder) - { - ListWorklistsFromFolder(worklists); - } - - for (std::vector::const_iterator it = worklists.begin(); it != worklists.end(); ++it) - { - if (deleteDelayInHours_ > 0 && it->IsOlderThan(deleteDelayInHours_)) - { - LOG(INFO) << "Deleting worklist " << it->id_ << " " << deleteDelayInHours_ << " hours after its creation"; - DeleteWorklist(it->id_); - } - else if (deleteWorklistsOnStableStudy_) - { - std::string studyInstanceUid; - std::string patientId; - - Orthanc::ParsedDicomFile parsed(it->dicomContent_); - - if (parsed.GetTagValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID) && - parsed.GetTagValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID)) - { - Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, "fake-id", "fake-id"); - const std::string& studyOrthancId = hasher.HashStudy(); - - Json::Value studyInfo; - if (OrthancPlugins::RestApiGet(studyInfo, "/studies/" + studyOrthancId, false)) - { - if (studyInfo["IsStable"].asBool()) - { - LOG(INFO) << "Deleting worklist " << it->id_ << " because its study is now stable"; - DeleteWorklist(it->id_); - } - } - } - } - } - } - } -} - - -static Orthanc::DicomToJsonFormat GetFormat(const OrthancPluginHttpRequest* request) -{ - std::map getArguments; - OrthancPlugins::GetGetArguments(getArguments, request); - - Orthanc::DicomToJsonFormat format = Orthanc::DicomToJsonFormat_Human; - - if (getArguments.find("format") != getArguments.end()) - { - format = Orthanc::StringToDicomToJsonFormat(getArguments["format"]); - } - - return format; -} - -static Orthanc::ParsedDicomFile* GetWorklist(const std::string& id) -{ - std::string fileContent; - - switch (worklistStorage_) - { - case WorklistStorageType_Folder: - { - boost::filesystem::path path = worklistDirectory_ / Orthanc::SystemToolbox::PathFromUtf8(id + ".wl"); // the id might be a filename from a file that was pushed by an external program (therefore, it can contain fancy characters) - if (!Orthanc::SystemToolbox::IsRegularFile(path)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Worklist not found"); - } - - Orthanc::SystemToolbox::ReadFile(fileContent, path); - break; - } - - case WorklistStorageType_OrthancDb: - { - std::string serializedWl; - if (!worklistsStore_->GetValue(serializedWl, id)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Worklist not found"); - } - - Json::Value jsonWl; - if (Orthanc::Toolbox::ReadJson(jsonWl, serializedWl)) - { - Worklist wl(id, jsonWl); - fileContent = wl.dicomContent_; - } - break; - } - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - return new Orthanc::ParsedDicomFile(fileContent); -} - -static void SerializeWorklistForApi(Json::Value& target, const std::string& id, const Orthanc::ParsedDicomFile& parsed, Orthanc::DicomToJsonFormat format) -{ - target["ID"] = id; - target["Tags"] = Json::objectValue; - parsed.DatasetToJson(target["Tags"], format, Orthanc::DicomToJsonFlags_None, 0); -} extern "C" { - - OrthancPluginErrorCode ListWorklists(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) - { - if (request->method != OrthancPluginHttpMethod_Get) - { - OrthancPlugins::AnswerMethodNotAllowed(output, "GET"); - } - else - { - Json::Value response = Json::arrayValue; - Orthanc::DicomToJsonFormat format = GetFormat(request); - - std::vector worklists; - - if (worklistStorage_ == WorklistStorageType_Folder) - { - ListWorklistsFromFolder(worklists); - } - else if (worklistStorage_ == WorklistStorageType_OrthancDb) - { - ListWorklistsFromDb(worklists); - } - - for (std::vector::const_iterator it = worklists.begin(); it != worklists.end(); ++it) - { - Orthanc::ParsedDicomFile parsed(it->dicomContent_); - Json::Value jsonWl; - SerializeWorklistForApi(jsonWl, it->id_, parsed, format); - - if (worklistStorage_ == WorklistStorageType_OrthancDb) - { - jsonWl["CreationDate"] = it->createdAt_; - } - - response.append(jsonWl); - } - - OrthancPlugins::AnswerJson(response, output); - } - - return OrthancPluginErrorCode_Success; - } - - - void CreateOrUpdateWorklist(std::string& worklistId, - bool defaultForceValue, - OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) - { - Json::Value body; - - if (!OrthancPlugins::ReadJson(body, request->body, request->bodySize)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected"); - } - - if (!body.isMember("Tags") || !body["Tags"].isObject()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'Tags' field is missing or not a JSON object"); - } - - bool force = defaultForceValue; - if (body.isMember("Force")) { - force = body["Force"].asBool(); - } - - Json::Value& jsonWorklist = body["Tags"]; - - if (!jsonWorklist.isMember("SpecificCharacterSet")) - { - jsonWorklist["SpecificCharacterSet"] = Orthanc::GetDicomSpecificCharacterSet(Orthanc::Encoding_Utf8); - } - - std::unique_ptr dicom(Orthanc::ParsedDicomFile::CreateFromJson(jsonWorklist, Orthanc::DicomFromJsonFlags_None, "")); - - if (!force) - { - if (!dicom->HasTag(Orthanc::DICOM_TAG_SCHEDULED_PROCEDURE_STEP_SEQUENCE)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'Tags' is missing a 'ScheduledProcedureStepSequence'. Use 'Force': true to bypass this check."); - } - Orthanc::DicomMap step; - if (!dicom->LookupSequenceItem(step, Orthanc::DicomPath::Parse("ScheduledProcedureStepSequence"), 0) || !step.HasTag(Orthanc::DICOM_TAG_MODALITY)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'ScheduledProcedureStepSequence' is missing a 'Modality' Use 'Force': true to bypass this check."); - } - if (!dicom->LookupSequenceItem(step, Orthanc::DicomPath::Parse("ScheduledProcedureStepSequence"), 0) || !step.HasTag(Orthanc::DICOM_TAG_SCHEDULED_PROCEDURE_STEP_START_DATE)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'ScheduledProcedureStepSequence' is missing a 'ScheduledProcedureStepStartDate' Use 'Force': true to bypass this check."); - } - } - - dicom->SetIfAbsent(Orthanc::DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, "1.2.276.0.7230010.3.1.0.1"); - - if (setStudyInstanceUidIfMissing_) - { - dicom->SetIfAbsent(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, Orthanc::FromDcmtkBridge::GenerateUniqueIdentifier(Orthanc::ResourceType_Study)); - } - - if (worklistId.empty()) - { - worklistId = Orthanc::Toolbox::GenerateUuid(); - } - - std::string dicomContent; - dicom->SaveToMemoryBuffer(dicomContent); - - switch (worklistStorage_) - { - case WorklistStorageType_Folder: - Orthanc::SystemToolbox::WriteFile(dicomContent.empty() ? NULL : dicomContent.c_str(), dicomContent.size(), - worklistDirectory_ / Orthanc::SystemToolbox::PathFromUtf8(worklistId + ".wl"), true); - break; - - case WorklistStorageType_OrthancDb: - { - Worklist wl(worklistId, dicomContent); - std::string serializedWl; - wl.Serialize(serializedWl); - - worklistsStore_->Store(worklistId, serializedWl); - break; - } - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - Json::Value response; - - response["ID"] = worklistId; - response["Path"] = "/worklists/" + worklistId; - - OrthancPlugins::AnswerJson(response, output); - } - - - OrthancPluginErrorCode GetPutDeleteWorklist(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) - { - std::string worklistId = std::string(request->groups[0]); - - if (request->method == OrthancPluginHttpMethod_Delete) - { - DeleteWorklist(worklistId); - - OrthancPlugins::AnswerString("{}", "application/json", output); - } - else if (request->method == OrthancPluginHttpMethod_Get) - { - Orthanc::DicomToJsonFormat format = GetFormat(request); - std::unique_ptr parsed(GetWorklist(worklistId)); - - Json::Value jsonWl; - SerializeWorklistForApi(jsonWl, worklistId, *parsed, format); - - OrthancPlugins::AnswerJson(jsonWl, output); - } - else if (request->method == OrthancPluginHttpMethod_Put) - { - CreateOrUpdateWorklist(worklistId, true, output, url, request); - } - else - { - OrthancPlugins::AnswerMethodNotAllowed(output, "DELETE,GET"); - } - - return OrthancPluginErrorCode_Success; - } - - - OrthancPluginErrorCode PostCreateWorklist(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) - { - if (request->method != OrthancPluginHttpMethod_Post) - { - OrthancPlugins::AnswerMethodNotAllowed(output, "POST"); - } - else - { - std::string worklistId; - CreateOrUpdateWorklist(worklistId, false, output, url, request); - } - return OrthancPluginErrorCode_Success; - } - - static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, - OrthancPluginResourceType resourceType, - const char *resourceId) - { - try - { - if (changeType == OrthancPluginChangeType_OrthancStarted) - { - Json::Value system; - bool hasKeyValueStores = (OrthancPlugins::RestApiGet(system, "/system", false) && system.isMember("Capabilities") && - system["Capabilities"].isMember("HasKeyValueStores") && system["Capabilities"]["HasKeyValueStores"].asBool()); - - if (worklistStorage_ == WorklistStorageType_OrthancDb && !hasKeyValueStores) - { - LOG(ERROR) << "The Orthanc DB plugin does not support Key Value Stores. It is therefore impossible to store the worklists in Orthanc Database"; - return OrthancPluginErrorCode_IncompatibleConfigurations; - } - - if (deleteDelayInHours_ > 0 && !hasKeyValueStores) - { - LOG(ERROR) << "The Orthanc DB plugin does not support Key Value Stores. It is therefore impossible to use the \"DeleteWorklistsDelay\" option"; - return OrthancPluginErrorCode_IncompatibleConfigurations; - } - - if (worklistStorage_ == WorklistStorageType_OrthancDb) - { - worklistsStore_.reset(new OrthancPlugins::KeyValueStore("worklists")); - } - - if (deleteDelayInHours_ > 0 || deleteWorklistsOnStableStudy_) - { - worklistHousekeeperThread_.reset(new boost::thread(WorklistHkWorkerThread)); - } - } - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "Exception: " << e.What(); - } - catch (...) - { - LOG(ERROR) << "Uncatched native exception"; - } - return OrthancPluginErrorCode_Success; - } - ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) { OrthancPlugins::SetGlobalContext(c, MODALITY_WORKLISTS_NAME); - Orthanc::Logging::InitializePluginContext(c, MODALITY_WORKLISTS_NAME); - + /* Check the version of the Orthanc core */ if (OrthancPluginCheckVersion(c) == 0) { @@ -737,29 +226,6 @@ return -1; } - { // init the OrthancFramework - static const char* const LOCALE = "Locale"; - static const char* const DEFAULT_ENCODING = "DefaultEncoding"; - - /** - * This function is a simplified version of function - * "Orthanc::OrthancInitialize()" that is executed when starting the - * Orthanc server. - **/ - OrthancPlugins::OrthancConfiguration globalConfig; - Orthanc::InitializeFramework(globalConfig.GetStringValue(LOCALE, ""), false /* loadPrivateDictionary */); - - std::string encoding; - if (globalConfig.LookupStringValue(encoding, DEFAULT_ENCODING)) - { - Orthanc::SetDefaultDicomEncoding(Orthanc::StringToEncoding(encoding.c_str())); - } - else - { - Orthanc::SetDefaultDicomEncoding(Orthanc::ORTHANC_DEFAULT_DICOM_ENCODING); - } - } - ORTHANC_PLUGINS_LOG_WARNING("Sample worklist plugin is initializing"); OrthancPluginSetDescription2(c, MODALITY_WORKLISTS_NAME, "Serve DICOM modality worklists from a folder with Orthanc."); @@ -771,52 +237,19 @@ bool enabled = worklists.GetBooleanValue("Enable", false); if (enabled) { - std::string folder; - if (worklists.LookupStringValue(folder, "Database") || worklists.LookupStringValue(folder, "Directory")) + if (worklists.LookupStringValue(folder_, "Database")) { - if (worklists.GetBooleanValue("SaveInOrthancDatabase", false)) - { - LOG(ERROR) << "Worklists plugin: you can not set the \"SaveInOrthancDatabase\" configuration to \"true\" once you have configured the \"Directory\" (or former \"Database\") configuration."; - return -1; - } - - worklistStorage_ = WorklistStorageType_Folder; - worklistDirectory_ = folder; //Orthanc::SystemToolbox::PathFromUtf8(folder); - LOG(WARNING) << "The database of worklists will be read from folder: " << folder; - } - else if (worklists.GetBooleanValue("SaveInOrthancDatabase", false)) - { - - worklistStorage_ = WorklistStorageType_OrthancDb; - ORTHANC_PLUGINS_LOG_WARNING("The database of worklists will be read from Orthanc Database"); + ORTHANC_PLUGINS_LOG_WARNING("The database of worklists will be read from folder: " + folder_); + OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), Callback); } else { - LOG(ERROR) << "The configuration option \"Worklists.Directory\" must contain a path"; + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"Worklists.Database\" must contain a path"); return -1; } - OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), WorklistCallback); - filterIssuerAet_ = worklists.GetBooleanValue("FilterIssuerAet", false); limitAnswers_ = worklists.GetUnsignedIntegerValue("LimitAnswers", 0); - - deleteWorklistsOnStableStudy_ = worklists.GetBooleanValue("DeleteWorklistsOnStableStudy", true); - hkIntervalInSeconds_ = worklists.GetUnsignedIntegerValue("HousekeepingInterval", 60); - deleteDelayInHours_ = worklists.GetUnsignedIntegerValue("DeleteWorklistsDelay", 0); - setStudyInstanceUidIfMissing_ = worklists.GetBooleanValue("SetStudyInstanceUidIfMissing", true); - - if (deleteDelayInHours_ > 0 && worklistStorage_ == WorklistStorageType_Folder) - { - LOG(ERROR) << "Worklists plugin: you can not set the \"DeleteWorklistsDelay\" configuration once you have configured the \"Directory\" (or former \"Database\") configuration. This feature only works once \"SaveInOrthancDatabase\" is set to true."; - return -1; - } - - OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback); - - OrthancPluginRegisterRestCallback(OrthancPlugins::GetGlobalContext(), "/worklists/create", PostCreateWorklist); - OrthancPluginRegisterRestCallback(OrthancPlugins::GetGlobalContext(), "/worklists/([^/]+)", GetPutDeleteWorklist); - OrthancPluginRegisterRestCallback(OrthancPlugins::GetGlobalContext(), "/worklists", ListWorklists); } else { @@ -830,13 +263,6 @@ ORTHANC_PLUGINS_API void OrthancPluginFinalize() { ORTHANC_PLUGINS_LOG_WARNING("Sample worklist plugin is finalizing"); - - worklistHousekeeperThreadShouldStop_ = true; - if (worklistHousekeeperThread_.get() != NULL && worklistHousekeeperThread_->joinable()) - { - worklistHousekeeperThread_->join(); - } - worklistHousekeeperThread_.reset(NULL); }