changeset 6441:bd0fb2559a7e

merge
author Alain Mazy <am@orthanc.team>
date Tue, 18 Nov 2025 17:33:29 +0100
parents 96676e815c1f (diff) 8b2ef7afec93 (current diff)
children f288ceb2a131
files
diffstat 5 files changed, 65 insertions(+), 783 deletions(-) [+]
line wrap: on
line diff
--- 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)
 ===========================
--- 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 
--- 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}"
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-/**
- * 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 <unicode/udata.h>
-
-// 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()
-  {
-  }
-}
--- 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 <boost/thread.hpp>
 #include <boost/filesystem.hpp>
 #include <json/value.h>
 #include <string.h>
 #include <iostream>
 #include <algorithm>
 
-static boost::filesystem::path worklistDirectory_;
+static std::string folder_;
 static bool filterIssuerAet_ = false;
 static unsigned int limitAnswers_ = 0;
-static std::unique_ptr<boost::thread> worklistHousekeeperThread_;
-static bool worklistHousekeeperThreadShouldStop_ = false;
-static bool deleteWorklistsOnStableStudy_ = true;
-static unsigned int hkIntervalInSeconds_ = 60;
-static unsigned int deleteDelayInHours_ = 24;
-static std::unique_ptr<OrthancPlugins::KeyValueStore> 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<Worklist>& target)
-{
-  assert(worklistStorage_ == WorklistStorageType_OrthancDb);
-  assert(worklistsStore_);
-  
-  std::unique_ptr<OrthancPlugins::KeyValueStore::Iterator> 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<Worklist>& 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<OrthancPlugins::FindMatcher> matcher(CreateMatcher(query, issuerAet));
 
-    std::vector<Worklist> 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<Worklist>::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<std::string>(parsedFilesCount) +
+                               " files, found " + boost::lexical_cast<std::string>(matchedWorklistCount) + " match(es)");
     }
-
-    LOG(INFO) << "Worklist C-Find: parsed " << boost::lexical_cast<std::string>(parsedFilesCount) <<
-                 " worklists, found " << boost::lexical_cast<std::string>(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<Worklist> worklists;
-
-      if (worklistStorage_ == WorklistStorageType_OrthancDb)
-      {
-        ListWorklistsFromDb(worklists);
-      }
-      else if (worklistStorage_ == WorklistStorageType_Folder)
-      {
-        ListWorklistsFromFolder(worklists);
-      }
-
-      for (std::vector<Worklist>::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<std::string, std::string> 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<Worklist> worklists;
-
-      if (worklistStorage_ == WorklistStorageType_Folder)
-      {
-        ListWorklistsFromFolder(worklists);
-      }
-      else if (worklistStorage_ == WorklistStorageType_OrthancDb)
-      {
-        ListWorklistsFromDb(worklists);
-      }
-
-      for (std::vector<Worklist>::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<Orthanc::ParsedDicomFile> 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<Orthanc::ParsedDicomFile> 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);
   }