changeset 7055:45817b3c1664 streaming

integration mainline->streaming
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 12 Aug 2026 08:33:57 +0200
parents 2a54e6538924 (current diff) 923bec010d54 (diff)
children 45205d0bda11
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancFramework/Sources/Toolbox.cpp OrthancFramework/UnitTestsSources/ToolboxTests.cpp OrthancServer/CMakeLists.txt OrthancServer/Sources/OrthancConfiguration.cpp OrthancServer/Sources/OrthancConfiguration.h OrthancServer/Sources/ServerJobs/ArchiveJob.cpp OrthancServer/Sources/main.cpp OrthancServer/UnitTestsSources/UnitTestsMain.cpp
diffstat 21 files changed, 912 insertions(+), 472 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Aug 10 14:59:05 2026 +0200
+++ b/NEWS	Wed Aug 12 08:33:57 2026 +0200
@@ -20,6 +20,7 @@
   - "SequentialDicomReaderWindowCapacity"
   And new metrics:
   - TODO list
+* Multiple paths to configuration files can now be provided, instead of a single file or a single folder
 * Orthanc will now refuse to start if you have "AuthenticationEnabled" and "RemoteAccessAllowed"
   set to true and have not defined any users in "RegisteredUsers".
 * New values for "OverwriteInstances" configuration. In previous versions, this configuration
@@ -41,7 +42,7 @@
   - Some line of log now contain additional contextual informations like the job id they relate to.
     A new "--logs-no-context" command line option can be used to get back to the previous behavior to
     keep backward compatibility and reduce the lines length.
-    Another "--logs-threadnames-in-context" command line option can be used to add more information in the 
+    Another "--logs-thread-names-in-context" command line option can be used to add more information in the 
     contexts wrt the thread that has requested a service.  This is mainly for developers.
 * C-Find SCP:
   - When Orthanc was requested a sequence in a C-Find query, Orthanc was actually not returning
@@ -53,6 +54,9 @@
 * Fix mismatches between the configuration default values and the documentation:
   - "HttpTimeout" was documented as 60 while its default value is 0 (no timeout).
   - "Name" was documented as "MyOrthanc" while its default value is "ORTHANC".
+* Orthanc no longer accepts DICOM association from modalities whose AET contains invalid characters.
+* Orthanc now ignores leading/trailing spaces in AET.
+
 
 REST API
 --------
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Wed Aug 12 08:33:57 2026 +0200
@@ -38,6 +38,10 @@
     list(APPEND ORTHANC_BOOST_COMPONENTS locale)
   endif()
 
+  if (ENABLE_BOOST_PROGRAM_OPTIONS)
+    list(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+  endif()
+
   list(APPEND ORTHANC_BOOST_COMPONENTS filesystem thread date_time regex iostreams)
   find_package(Boost COMPONENTS ${ORTHANC_BOOST_COMPONENTS})
 
@@ -441,6 +445,27 @@
   endif()
 
   
+  ##
+  ## Configuration of boost::program_options (new in Orthanc 1.13.0)
+  ##
+
+  if (ENABLE_BOOST_PROGRAM_OPTIONS)
+    list(APPEND BOOST_SOURCES
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/cmdline.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/config_file.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/convert.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/options_description.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/parsers.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/positional_options.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/split.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/utf8_codecvt_facet.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/value_semantic.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/variables_map.cpp
+      # ${BOOST_SOURCES_DIR}/libs/program_options/src/winmain.cpp
+      )
+  endif()
+
+
   source_group(ThirdParty\\boost REGULAR_EXPRESSION ${BOOST_SOURCES_DIR}/.*)
 
 endif()
--- a/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake	Wed Aug 12 08:33:57 2026 +0200
@@ -360,6 +360,27 @@
   endif()
 
   
+  ##
+  ## Configuration of boost::program_options (new in Orthanc 1.13.0)
+  ##
+
+  if (ENABLE_BOOST_PROGRAM_OPTIONS)
+    list(APPEND BOOST_SOURCES
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/cmdline.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/config_file.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/convert.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/options_description.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/parsers.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/positional_options.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/split.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/utf8_codecvt_facet.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/value_semantic.cpp
+      ${BOOST_SOURCES_DIR}/libs/program_options/src/variables_map.cpp
+      # ${BOOST_SOURCES_DIR}/libs/program_options/src/winmain.cpp
+      )
+  endif()
+
+
   source_group(ThirdParty\\boost REGULAR_EXPRESSION ${BOOST_SOURCES_DIR}/.*)
 
 endif()
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Aug 12 08:33:57 2026 +0200
@@ -137,6 +137,7 @@
 set(ENABLE_DCMTK_NETWORKING OFF CACHE INTERNAL "Enable DICOM networking in DCMTK")
 set(ENABLE_DCMTK_TRANSCODING OFF CACHE INTERNAL "Enable DICOM transcoding in DCMTK")
 set(ENABLE_OPENSSL_ENGINES OFF CACHE INTERNAL "Enable support of engines in OpenSSL")
+set(ENABLE_BOOST_PROGRAM_OPTIONS OFF CACHE BOOL "Enable support for the boost::program_options library")  # New in Orthanc 1.13.0
 
 set(ORTHANC_SANDBOXED OFF CACHE INTERNAL
   "Whether Orthanc runs inside a sandboxed environment (such as Google NaCl or WebAssembly)")
--- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -328,6 +328,15 @@
         calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
       }
 
+      if (!Toolbox::IsValidAet(remoteAet))
+      {
+        LOG(ERROR) << "Association Received from an invalid AET (sanitized: " << Toolbox::NormalizeAet(remoteAet) << ")" 
+                   << " on IP " << remoteIp << ", rejecting the association";
+        return NULL;
+      }
+
+      remoteAet = Toolbox::NormalizeAet(remoteAet);  // removes leading/trailing spaces that are not significant according to the standard
+
       CLOG(INFO, DICOM) << "Association Received from AET " << remoteAet 
                         << " on IP " << remoteIp;
 
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -1280,7 +1280,10 @@
                                struct mg_connection *connection,
                                const struct mg_request_info *request)
   {
-    server.UpdateCurrentThreadName();
+    // Since we don't control the thread creation ourselves, we set and clear 
+    // the thread name everytime otherwise, the thread name will persist even 
+    // after a call to /tools/reset and Orthanc would stop because the same thread name is used multiple times
+    Logging::ScopedCurrentThreadNameSetter setter(server.GetCurrentThreadName()); 
 
     std::unique_ptr<MetricsRegistry::AvailableResourcesDecounter> counter(server.CreateAvailableHttpThreadsDecounter());
 
@@ -1825,8 +1828,11 @@
 
   void HttpServer::Start()
   {
-    // reset thread counter used to generate HTTP thread names.
-    threadCounter_ = 0;
+    {
+      // reset thread counter used to generate HTTP thread names.
+      boost::unique_lock<boost::shared_mutex> lock(threadCounterMutex_);
+      threadCounter_ = 0;
+    }
 
 #if ORTHANC_ENABLE_MONGOOSE == 1
     CLOG(INFO, HTTP) << "Starting embedded Web server using Mongoose";
@@ -2467,13 +2473,25 @@
   }
 
 
-  void HttpServer::UpdateCurrentThreadName()
+  std::string HttpServer::GetCurrentThreadName()
   {
     // threads are created in CivetWeb -> assign them a name the first time they are used
-    if (!Logging::HasCurrentThreadName())
+    const boost::thread::id threadId = boost::this_thread::get_id();
+
+    boost::upgrade_lock<boost::shared_mutex> readerLock(threadCounterMutex_); // reader lock to check if the threadId has already been registered
+
+    if (threadNames_.find(threadId) == threadNames_.end())
     {
-      boost::mutex::scoped_lock lock(threadCounterMutex_);
-      Logging::SetCurrentThreadName(std::string("HTTP-") + boost::lexical_cast<std::string>(threadCounter_++));
+      boost::upgrade_to_unique_lock<boost::shared_mutex> writerLock(readerLock);
+      
+      std::string thisThreadName = std::string("HTTP-") + boost::lexical_cast<std::string>(threadCounter_++);
+      threadNames_[threadId] = thisThreadName;
+      
+      return thisThreadName;
+    }
+    else
+    {
+      return threadNames_[threadId];
     }
   }
 
--- a/OrthancFramework/Sources/HttpServer/HttpServer.h	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.h	Wed Aug 12 08:33:57 2026 +0200
@@ -54,6 +54,7 @@
 #include <stdint.h>
 #include <boost/shared_ptr.hpp>
 #include <boost/filesystem.hpp>
+#include <boost/thread.hpp>
 
 
 namespace Orthanc
@@ -118,8 +119,9 @@
     unsigned int requestTimeout_;  // In seconds
     std::unique_ptr<MetricsRegistry::SharedMetrics> availableHttpThreadsMetrics_;  // New in Orthanc 1.12.9
 
-    boost::mutex threadCounterMutex_;  // New in Orthanc 1.12.9
-    uint16_t threadCounter_;           // Introduced as a global, static variable in Orthanc 1.12.2
+    boost::shared_mutex threadCounterMutex_;  // protects threadCounter_ and threadNames_;
+    uint16_t threadCounter_;
+    std::map<boost::thread::id, std::string> threadNames_;
 
     // New in Orthanc 1.12.11
     bool    hasMaxBodySize_;
@@ -247,7 +249,7 @@
     // Can return NULL if SetMetricsRegistry() was not call beforehand
     MetricsRegistry::AvailableResourcesDecounter* CreateAvailableHttpThreadsDecounter();
 
-    void UpdateCurrentThreadName();
+    std::string GetCurrentThreadName();
 
     void SetMaxBodySize(uint64_t size);
 
--- a/OrthancFramework/Sources/SystemToolbox.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/Sources/SystemToolbox.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -227,8 +227,8 @@
   {
     if (!IsRegularFile(path))
     {
-      throw OrthancException(ErrorCode_RegularFileExpected,
-                             "The path does not point to a regular file: " + PathToUtf8(path), log);
+      throw OrthancException(ErrorCode_RegularFileExpected, 
+                             "Not a regular file or file not found: " + PathToUtf8(path), log);
     }
 
     try
@@ -258,11 +258,13 @@
     }
     catch (boost::filesystem::filesystem_error&)
     {
-      throw OrthancException(ErrorCode_InexistentFile);
+      throw OrthancException(ErrorCode_InexistentFile, 
+                             "File not found: " + PathToUtf8(path));
     }
     catch (...)  // To catch "std::system_error&" in C++11
     {
-      throw OrthancException(ErrorCode_InexistentFile);
+      throw OrthancException(ErrorCode_InexistentFile, 
+                             "File not found: " + PathToUtf8(path));
     }
   }
 
@@ -273,8 +275,8 @@
   {
     if (!IsRegularFile(path))
     {
-      throw OrthancException(ErrorCode_RegularFileExpected,
-                             "The path does not point to a regular file: " + PathToUtf8(path));
+      throw OrthancException(ErrorCode_RegularFileExpected, 
+                             "Not a regular file or file not found: " + PathToUtf8(path));
     }
 
     try
@@ -283,7 +285,8 @@
       f.open(path, std::ifstream::in | std::ifstream::binary);
       if (!f.good())
       {
-        throw OrthancException(ErrorCode_InexistentFile);
+        throw OrthancException(ErrorCode_InexistentFile,
+                               "File not found: " + PathToUtf8(path));
       }
 
       bool full = true;
@@ -314,11 +317,13 @@
     }
     catch (boost::filesystem::filesystem_error&)
     {
-      throw OrthancException(ErrorCode_InexistentFile);
+      throw OrthancException(ErrorCode_InexistentFile, 
+                             "File not found: " + PathToUtf8(path));
     }
     catch (...)  // To catch "std::system_error&" in C++11
     {
-      throw OrthancException(ErrorCode_InexistentFile);
+      throw OrthancException(ErrorCode_InexistentFile, 
+                             "File not found: " + PathToUtf8(path));
     }
   }
 
@@ -414,7 +419,8 @@
       }
       else
       {
-        throw OrthancException(ErrorCode_RegularFileExpected);
+        throw OrthancException(ErrorCode_RegularFileExpected, 
+                               "Not a regular file or file not found: " + PathToUtf8(path));
       }
     }
   }
@@ -428,11 +434,13 @@
     }
     catch (boost::filesystem::filesystem_error&)
     {
-      throw OrthancException(ErrorCode_InexistentFile);
+      throw OrthancException(ErrorCode_InexistentFile,
+                             "File not found: " + PathToUtf8(path));
     }
     catch (...)  // To catch "std::system_error&" in C++11
     {
-      throw OrthancException(ErrorCode_InexistentFile);
+      throw OrthancException(ErrorCode_InexistentFile, 
+                             "File not found: " + PathToUtf8(path));
     }
   }
 
@@ -1068,16 +1076,16 @@
     
     if (!IsRegularFile(path))
     {
-      throw OrthancException(ErrorCode_RegularFileExpected,
-                             "The path does not point to a regular file: " + SystemToolbox::PathToUtf8(path));
+      throw OrthancException(ErrorCode_RegularFileExpected, 
+                             "Not a regular file or file not found: " + PathToUtf8(path));
     }
 
     boost::filesystem::ifstream f;
     f.open(path, std::ifstream::in | std::ifstream::binary);
     if (!f.good())
     {
-      throw OrthancException(ErrorCode_InexistentFile, 
-                             "File not found: " + SystemToolbox::PathToUtf8(path));
+      throw OrthancException(ErrorCode_InexistentFile,
+                             "File not found: " + PathToUtf8(path));
     }
 
     uint64_t fileSize = static_cast<uint64_t>(GetStreamSize(f));
--- a/OrthancFramework/Sources/Toolbox.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -3022,6 +3022,60 @@
   }
 
 
+  bool Toolbox::IsValidAet(const std::string& aet)
+  {
+    if (aet.size() == 0)
+    {
+      return false;
+    }
+
+    const uint8_t* p = reinterpret_cast<const uint8_t*>(aet.c_str());
+    size_t spacesCount = 0;
+
+    for (size_t i = 0; i < aet.size(); ++i, ++p)
+    {
+      if (iscntrl(*p) || *p == '\\') // formaly, only LF, FF, CR and ESC and \ are invalid but let's refuse any control character to be extra safe.
+      {
+        return false;
+      }
+      else if (*p == ' ')
+      {
+        ++spacesCount;
+      }
+    }
+
+    if (spacesCount == aet.size()) // an AET made of only spaces is invalid
+    {
+      return false;
+    }
+
+    return true;
+  }
+
+
+  // replaces any invalid char by * such that the AET is safe to log
+  // + removes leading and trailing spaces that must be ignored
+  std::string Toolbox::NormalizeAet(const std::string& aet)
+  {
+    std::string normalizedAet;
+
+    for (size_t i = 0; i < aet.size(); ++i)
+    {
+      if (iscntrl(aet[i]) ||
+          aet[i] == '\\')
+      {
+        normalizedAet.push_back('*');
+      }
+      else
+      {
+        normalizedAet.push_back(aet[i]);
+      }
+    }
+
+    return StripSpaces(normalizedAet); // remove any leading/trailing spaces (all leading \t\n\r have already been sanitized before)
+  }
+
+
   std::string Toolbox::NormalizePath(const std::string& utf8,
                                      bool allowUtf8,
                                      bool allowSlashes)
--- a/OrthancFramework/Sources/Toolbox.h	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Wed Aug 12 08:33:57 2026 +0200
@@ -411,6 +411,10 @@
 
     static bool IsValidUtf8(const std::string& s);
 
+    static bool IsValidAet(const std::string& aet);
+
+    static std::string NormalizeAet(const std::string& aet);
+
     static std::string NormalizePath(const std::string& utf8,
                                      bool allowUtf8,
                                      bool allowSlashes);
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -461,6 +461,28 @@
   ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xF0\x28\x8C\x28"));
 }
 
+TEST(Toolbox, IsValidAet)
+{
+  ASSERT_TRUE(Orthanc::Toolbox::IsValidAet("ABC_DEF"));
+  ASSERT_FALSE(Orthanc::Toolbox::IsValidAet(""));
+  ASSERT_FALSE(Orthanc::Toolbox::IsValidAet(" "));
+  ASSERT_FALSE(Orthanc::Toolbox::IsValidAet("  "));
+  ASSERT_FALSE(Orthanc::Toolbox::IsValidAet("NEW_LINE\n"));
+  ASSERT_FALSE(Orthanc::Toolbox::IsValidAet("CAR_RET\r"));
+  ASSERT_FALSE(Orthanc::Toolbox::IsValidAet("\\SLASH"));
+  ASSERT_FALSE(Orthanc::Toolbox::IsValidAet(" \x01CNTRL"));
+}
+
+TEST(Toolbox, NormalizeAet)
+{
+  ASSERT_EQ("ABC_DEF", Orthanc::Toolbox::NormalizeAet(" ABC_DEF "));
+  ASSERT_EQ("NEW_LINE*", Orthanc::Toolbox::NormalizeAet("NEW_LINE\n"));
+  ASSERT_EQ("WITH MIDDLE SPACE", Orthanc::Toolbox::NormalizeAet("WITH MIDDLE SPACE"));
+  ASSERT_EQ("CAR_RET*", Orthanc::Toolbox::NormalizeAet("CAR_RET\r"));
+  ASSERT_EQ("SLASH*", Orthanc::Toolbox::NormalizeAet("SLASH\\"));
+  ASSERT_EQ("CNTRL*", Orthanc::Toolbox::NormalizeAet("CNTRL\x01"));
+}
+
 TEST(Toolbox, BoundMemorySizeToCurrentArchitecture)
 {
   if (sizeof(void*) == 4)
--- a/OrthancServer/CMakeLists.txt	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancServer/CMakeLists.txt	Wed Aug 12 08:33:57 2026 +0200
@@ -32,6 +32,7 @@
 include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake)
 
 # Enable all the optional components of the Orthanc framework
+set(ENABLE_BOOST_PROGRAM_OPTIONS ON)
 set(ENABLE_CRYPTO_OPTIONS ON)
 set(ENABLE_DCMTK ON)
 set(ENABLE_DCMTK_NETWORKING ON)
@@ -260,6 +261,7 @@
 
 set(ORTHANC_EMBEDDED_FILES
   CONFIGURATION_SAMPLE              ${CMAKE_SOURCE_DIR}/Resources/Configuration.json
+  ADVANCED_CONFIGURATION_SAMPLE     ${CMAKE_SOURCE_DIR}/Resources/AdvancedConfiguration.json
   DICOM_CONFORMANCE_STATEMENT       ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
   FONT_UBUNTU_MONO_BOLD_16          ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
   LUA_TOOLBOX                       ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginException.h	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginException.h	Wed Aug 12 08:33:57 2026 +0200
@@ -48,11 +48,16 @@
       errorCode, #errorCode " triggered from " __ORTHANC_FILE__ ":"       \
       PLUGIN_ORTHANC_EXCEPTION_STRINGIFY_LINE(__LINE__))
 
+#  define PLUGIN_THROW_WITH_FILE_AND_LINE_INFO_HELPER(errorCode, errorCodeStr)                \
+    throw ::Orthanc::OrthancException( \
+      errorCode, "Plugin error code " + errorCodeStr + " triggered from " __ORTHANC_FILE__ ":"       \
+      PLUGIN_ORTHANC_EXCEPTION_STRINGIFY_LINE(__LINE__))
+
 #  define ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code)                   \
-  PLUGIN_THROW_WITH_FILE_AND_LINE_INFO(static_cast<ORTHANC_PLUGINS_ERROR_ENUMERATION>(code))    
+  PLUGIN_THROW_WITH_FILE_AND_LINE_INFO_HELPER(static_cast<ORTHANC_PLUGINS_ERROR_ENUMERATION>(code), boost::lexical_cast<std::string>(code))    
 
 #  define ORTHANC_PLUGINS_THROW_EXCEPTION(code)                           \
-  PLUGIN_THROW_WITH_FILE_AND_LINE_INFO(ORTHANC_PLUGINS_GET_ERROR_CODE(code))    
+  PLUGIN_THROW_WITH_FILE_AND_LINE_INFO_HELPER(ORTHANC_PLUGINS_GET_ERROR_CODE(code), boost::lexical_cast<std::string>(ORTHANC_PLUGINS_GET_ERROR_CODE(code)))    
 
 #else // the PluginException does not accept a detail argument
 #  define ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code)                   \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/AdvancedConfiguration.json	Wed Aug 12 08:33:57 2026 +0200
@@ -0,0 +1,9 @@
+{
+  /**
+   * Advanced configuration options of Orthanc
+   *
+   * This file gathers advanced options that were part of the main
+   * configuration file until Orthanc 1.12.11.
+   **/
+
+}
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -48,45 +48,70 @@
 static const char* const JOBS_ENGINE_THREADS_COUNT = "JobsEngineThreadsCount";
 static const char* const DICOM_LOSSY_TRANSCODING_QUALITY = "DicomLossyTranscodingQuality";
 
-static Json::Value defaultConfiguration;
-
 namespace Orthanc
 {
-  static void AddFileToConfiguration(Json::Value& target,
-                                     const boost::filesystem::path& path)
+  static void ReadConfigurationFromString(Json::Value& target,
+                                          const std::string& content)
   {
     std::map<std::string, std::string> env;
     SystemToolbox::GetEnvironmentVariables(env);
-    
-    LOG(WARNING) << "Reading the configuration from: " << SystemToolbox::PathToUtf8(path);
 
-    Json::Value config;
-
-    {
-      std::string content;
-      SystemToolbox::ReadFile(content, path);
+    std::string substituted = Toolbox::SubstituteVariables(content, env);
 
-      content = Toolbox::SubstituteVariables(content, env);
-
-      Json::Value tmp;
-      if (!Toolbox::ReadJson(tmp, content) ||
-          tmp.type() != Json::objectValue)
-      {
-        throw OrthancException(ErrorCode_BadJson,
-                               "The configuration file does not follow the JSON syntax: " + SystemToolbox::PathToUtf8(path));
-      }
-
-      Toolbox::CopyJsonWithoutComments(config, tmp);
+    Json::Value tmp;
+    if (!Toolbox::ReadJson(tmp, substituted) ||
+        tmp.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadJson, "The configuration file does not follow the JSON syntax");
     }
 
-    if (target.size() == 0)
+    Toolbox::CopyJsonWithoutComments(target, tmp);
+  }
+
+
+  static void ReadConfigurationFromFile(Json::Value& target,
+                                        const boost::filesystem::path& path)
+  {
+    LOG(WARNING) << "Reading the configuration file: " << SystemToolbox::PathToUtf8(path);
+
+    std::string content;
+    SystemToolbox::ReadFile(content, path);
+
+    ReadConfigurationFromString(target, content);
+  }
+
+
+  static void ReadDefaultConfiguration(Json::Value& target)
+  {
+#if ORTHANC_STANDALONE == 1
+    std::string content;
+    GetFileResource(content, ServerResources::CONFIGURATION_SAMPLE);
+
+    ReadConfigurationFromString(target, content);
+#else
+    // In a non-standalone build, we use the
+    // "Resources/Configuration.json" from the Orthanc source code
+
+    boost::filesystem::path p = ORTHANC_PATH;
+    p /= "Resources";
+    p /= "Configuration.json";
+
+    ReadConfigurationFromFile(target, p);
+#endif
+  }
+
+
+  static void MergeConfigurations(Json::Value& target,
+                                  const Json::Value& source)
+  {
+    if (target.type() != Json::objectValue ||
+        source.type() != Json::objectValue)
     {
-      target = config;
+      throw OrthancException(ErrorCode_InternalError);
     }
     else
     {
-      // Merge the newly-added file with the previous content of "target"
-      Json::Value::Members members = config.getMemberNames();
+      Json::Value::Members members = source.getMemberNames();
       for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
       {
         if (target.isMember(members[i]))
@@ -97,24 +122,21 @@
         }
         else
         {
-          target[members[i]] = config[members[i]];
+          target[members[i]] = source[members[i]];
         }
       }
     }
   }
 
-    
-  static void ScanFolderForConfiguration(Json::Value& target,
-                                         const boost::filesystem::path& folder)
-  {
-    using namespace boost::filesystem;
 
-    LOG(WARNING) << "Scanning folder \"" << Orthanc::SystemToolbox::PathToUtf8(folder) << "\" for configuration files";
+  static void ReadConfigurationsFromFolder(Json::Value& target,
+                                           const boost::filesystem::path& folder)
+  {
+    LOG(WARNING) << "Scanning folder for configuration files: " << Orthanc::SystemToolbox::PathToUtf8(folder);
 
-    directory_iterator end_it; // default construction yields past-the-end
-    for (directory_iterator it(folder);
-         it != end_it;
-         ++it)
+    boost::filesystem::directory_iterator end_it; // default construction yields past-the-end
+
+    for (boost::filesystem::directory_iterator it(folder); it != end_it; ++it)
     {
       if (!is_directory(it->status()))
       {
@@ -123,70 +145,34 @@
 
         if (extension == ".json")
         {
-          AddFileToConfiguration(target, it->path());
+          Json::Value config;
+          ReadConfigurationFromFile(config, it->path());
+          MergeConfigurations(target, config);
         }
       }
     }
   }
 
-    
-  static void ReadConfiguration(Json::Value& target, 
-                                const boost::filesystem::path &configurationFile)
+
+  static void ReadConfiguration(Json::Value& target,
+                                const std::list<boost::filesystem::path>& configurationPaths)
   {
-    // lazy loading of the default configuration
-    if (defaultConfiguration.empty())
-    {
-      std::string defaultConfigurationContent;
-      GetFileResource(defaultConfigurationContent, ServerResources::CONFIGURATION_SAMPLE);
-
-      Json::Value tmp;
-      if (!Toolbox::ReadJson(tmp, defaultConfigurationContent) ||
-          tmp.type() != Json::objectValue)
-      {
-        throw OrthancException(ErrorCode_InternalError, "The default configuration file does not follow the JSON syntax !!");
-      }
-
-      Toolbox::CopyJsonWithoutComments(defaultConfiguration, tmp);
-    }
-
     target = Json::objectValue;
 
-    if (!configurationFile.empty())
+    for (std::list<boost::filesystem::path>::const_iterator it = configurationPaths.begin();
+         it != configurationPaths.end(); ++it)
     {
-      if (!boost::filesystem::exists(configurationFile))
+      if (boost::filesystem::is_directory(*it))
       {
-        throw OrthancException(ErrorCode_InexistentFile,
-                               "Inexistent path to configuration: " +
-                               SystemToolbox::PathToUtf8(configurationFile));
-      }
-      
-      if (boost::filesystem::is_directory(configurationFile))
-      {
-        ScanFolderForConfiguration(target, configurationFile);
+        ReadConfigurationsFromFolder(target, *it);
       }
       else
       {
-        AddFileToConfiguration(target, configurationFile);
+        Json::Value config;
+        ReadConfigurationFromFile(config, *it);
+        MergeConfigurations(target, config);
       }
     }
-    else
-    {
-#if ORTHANC_STANDALONE == 1
-      // No default path for the standalone configuration
-      LOG(WARNING) << "Using the default Orthanc configuration";
-      return;
-
-#else
-      // In a non-standalone build, we use the
-      // "Resources/Configuration.json" from the Orthanc source code
-
-      boost::filesystem::path p = ORTHANC_PATH;
-      p /= "Resources";
-      p /= "Configuration.json";
-
-      AddFileToConfiguration(target, p);
-#endif
-    }
   }
 
 
@@ -205,6 +191,13 @@
   }
 
 
+  OrthancConfiguration::OrthancConfiguration() :
+    serverIndex_(NULL)
+  {
+    ReadDefaultConfiguration(defaultConfiguration_);
+  }
+
+
   void OrthancConfiguration::LoadModalitiesFromJson(const Json::Value& source)
   {
     modalities_.clear();
@@ -283,9 +276,9 @@
     else
     {
       // Modalities are stored in the configuration files
-      if (json_.isMember(DICOM_MODALITIES))
+      if (userConfiguration_.isMember(DICOM_MODALITIES))
       {
-        LoadModalitiesFromJson(json_[DICOM_MODALITIES]);
+        LoadModalitiesFromJson(userConfiguration_[DICOM_MODALITIES]);
       }
       else
       {
@@ -299,9 +292,9 @@
     // default values
     jobsEngineThreadsCount_["ResourceModification"] = 1;
 
-    if (json_.isMember(JOBS_ENGINE_THREADS_COUNT))
+    if (userConfiguration_.isMember(JOBS_ENGINE_THREADS_COUNT))
     {
-      const Json::Value& source = json_[JOBS_ENGINE_THREADS_COUNT];
+      const Json::Value& source = userConfiguration_[JOBS_ENGINE_THREADS_COUNT];
       if (source.type() != Json::objectValue)
       {
         throw OrthancException(ErrorCode_BadFileFormat,
@@ -343,6 +336,28 @@
     return workersThread;
   }
 
+  void OrthancConfiguration::LoadOrthancAET()
+  {
+    std::string dicomAet = GetStringParameter(ORTHANC_CONFIG_DICOM_AET);
+    
+    if (!Toolbox::IsValidAet(dicomAet))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, std::string("The ") + ORTHANC_CONFIG_DICOM_AET + " contains characters that are not valid for an AET");
+    }
+
+    orthancDicomAet_ = Toolbox::NormalizeAet(dicomAet);
+  }
+
+  std::string OrthancConfiguration::GetOrthancAET() const
+  {
+    if (orthancDicomAet_.empty())
+    {
+      throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    return orthancDicomAet_;    
+  }
+
   void OrthancConfiguration::LoadPeers()
   {
     if (GetBooleanParameter(ORTHANC_PEERS_IN_DB))
@@ -371,9 +386,9 @@
     else
     {
       // Peers are stored in the configuration files
-      if (json_.isMember(ORTHANC_PEERS))
+      if (userConfiguration_.isMember(ORTHANC_PEERS))
       {
-        LoadPeersFromJson(json_[ORTHANC_PEERS]);
+        LoadPeersFromJson(userConfiguration_[ORTHANC_PEERS]);
       }
       else
       {
@@ -437,9 +452,9 @@
     {
       // Modalities are stored in the configuration files
       if (!modalities_.empty() ||
-          json_.isMember(DICOM_MODALITIES))
+          userConfiguration_.isMember(DICOM_MODALITIES))
       {
-        SaveModalitiesToJson(json_[DICOM_MODALITIES]);
+        SaveModalitiesToJson(userConfiguration_[DICOM_MODALITIES]);
       }
     }
   }
@@ -469,9 +484,9 @@
     {
       // Peers are stored in the configuration files
       if (!peers_.empty() ||
-          json_.isMember(ORTHANC_PEERS))
+          userConfiguration_.isMember(ORTHANC_PEERS))
       {
-        SavePeersToJson(json_[ORTHANC_PEERS]);
+        SavePeersToJson(userConfiguration_[ORTHANC_PEERS]);
       }
     }
   }
@@ -484,19 +499,20 @@
   }
 
 
-  bool OrthancConfiguration::LookupStringParameter(std::string& target,
-                                                   const std::string& parameter) const
+  static bool LookupStringParameterInternal(std::string& target,
+                                            const Json::Value& config,
+                                            const std::string& parameter)
   {
-    if (json_.isMember(parameter))
+    if (config.isMember(parameter))
     {
-      if (json_[parameter].type() != Json::stringValue)
+      if (config[parameter].type() != Json::stringValue)
       {
         throw OrthancException(ErrorCode_BadParameterType,
                                "The configuration option \"" + parameter + "\" must be a string");
       }
       else
       {
-        target = json_[parameter].asString();
+        target = config[parameter].asString();
         return true;
       }
     }
@@ -507,6 +523,13 @@
   }
 
 
+  bool OrthancConfiguration::LookupStringParameter(std::string& target,
+                                                   const std::string& parameter) const
+  {
+    return LookupStringParameterInternal(target, userConfiguration_, parameter);
+  }
+
+
   std::string OrthancConfiguration::GetStringParameter(const std::string& parameter) const
   {
     std::string value;
@@ -514,30 +537,32 @@
     {
       return value;
     }
-    else if (defaultConfiguration.isMember(parameter) && defaultConfiguration[parameter].type() == Json::stringValue)
+    else if (LookupStringParameterInternal(value, defaultConfiguration_, parameter))
     {
-      return defaultConfiguration[parameter].asString();
+      return value;
     }
     else
     {
-      throw OrthancException(ErrorCode_InternalError, std::string("No or invalid default parameter found in the default configuration for '") + parameter + "'");
+      throw OrthancException(ErrorCode_InternalError,
+                             "No or invalid default parameter found in the default configuration for '" + parameter + "'");
     }
   }
 
-    
-  bool OrthancConfiguration::LookupIntegerParameter(int& target,
-                                                    const std::string& parameter) const
+
+  static bool LookupIntegerParameterInternal(int& target,
+                                             const Json::Value& config,
+                                             const std::string& parameter)
   {
-    if (json_.isMember(parameter))
+    if (config.isMember(parameter))
     {
-      if (json_[parameter].type() != Json::intValue)
+      if (config[parameter].type() != Json::intValue)
       {
         throw OrthancException(ErrorCode_BadParameterType,
                                "The configuration option \"" + parameter + "\" must be an integer");
       }
       else
       {
-        target = json_[parameter].asInt();
+        target = config[parameter].asInt();
         return true;
       }
     }
@@ -548,13 +573,41 @@
   }
 
 
-    
-  bool OrthancConfiguration::LookupUnsignedIntegerParameter(unsigned int& target,
-                                                            const std::string& parameter) const
+  bool OrthancConfiguration::LookupIntegerParameter(int& target,
+                                                    const std::string& parameter) const
+  {
+    return LookupIntegerParameterInternal(target, userConfiguration_, parameter);
+  }
+
+
+  bool OrthancConfiguration::GetIntegerParameter(int& target,
+                                                 const std::string& parameter) const
   {
     int v;
+
     if (LookupIntegerParameter(v, parameter))
     {
+      return v;
+    }
+    else if (LookupIntegerParameterInternal(v, defaultConfiguration_, parameter))
+    {
+      return v;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "No or invalid default parameter found in the default configuration for '" + parameter + "'");
+    }
+  }
+
+    
+  static bool LookupUnsignedIntegerParameterInternal(unsigned int& target,
+                                                     const Json::Value& config,
+                                                     const std::string& parameter)
+  {
+    int v;
+    if (LookupIntegerParameterInternal(v, config, parameter))
+    {
       if (v < 0)
       {
         throw OrthancException(ErrorCode_ParameterOutOfRange,
@@ -573,6 +626,13 @@
   }
 
 
+  bool OrthancConfiguration::LookupUnsignedIntegerParameter(unsigned int& target,
+                                                            const std::string& parameter) const
+  {
+    return LookupUnsignedIntegerParameterInternal(target, userConfiguration_, parameter);
+  }
+
+
   unsigned int OrthancConfiguration::GetUnsignedIntegerParameter(const std::string& parameter) const
   {
     unsigned int v;
@@ -580,23 +640,25 @@
     {
       return v;
     }
-    else if (defaultConfiguration.isMember(parameter) && defaultConfiguration[parameter].type() == Json::intValue)
+    else if (LookupUnsignedIntegerParameterInternal(v, defaultConfiguration_, parameter))
     {
-      return static_cast<unsigned int>(defaultConfiguration[parameter].asInt());
+      return v;
     }
     else
     {
-      throw OrthancException(ErrorCode_InternalError, std::string("No or invalid default parameter found in the default configuration for '") + parameter + "'");
+      throw OrthancException(ErrorCode_InternalError,
+                             "No or invalid default parameter found in the default configuration for '" + parameter + "'");
     }
   }
 
 
-  bool OrthancConfiguration::LookupBooleanParameter(bool& target,
-                                                    const std::string& parameter) const
+  static bool LookupBooleanParameterInternal(bool& target,
+                                             const Json::Value& config,
+                                             const std::string& parameter)
   {
-    if (json_.isMember(parameter))
+    if (config.isMember(parameter))
     {
-      if (json_[parameter].type() != Json::booleanValue)
+      if (config[parameter].type() != Json::booleanValue)
       {
         throw OrthancException(ErrorCode_BadParameterType,
                                "The configuration option \"" + parameter +
@@ -604,7 +666,7 @@
       }
       else
       {
-        target = json_[parameter].asBool();
+        target = config[parameter].asBool();
         return true;
       }
     }
@@ -615,6 +677,13 @@
   }
 
 
+  bool OrthancConfiguration::LookupBooleanParameter(bool& target,
+                                                    const std::string& parameter) const
+  {
+    return LookupBooleanParameterInternal(target, userConfiguration_, parameter);
+  }
+
+
   bool OrthancConfiguration::GetBooleanParameter(const std::string& parameter) const
   {
     bool value;
@@ -622,52 +691,76 @@
     {
       return value;
     }
-    else if (defaultConfiguration.isMember(parameter) && defaultConfiguration[parameter].type() == Json::booleanValue)
+    else if (LookupBooleanParameterInternal(value, defaultConfiguration_, parameter))
     {
-      return defaultConfiguration[parameter].asBool();
+      return value;
     }
     else
     {
-      throw OrthancException(ErrorCode_InternalError, std::string("No or invalid default parameter found in the default configuration for '") + parameter + "'");
+      throw OrthancException(ErrorCode_InternalError,
+                             "No or invalid default parameter found in the default configuration for '" + parameter + "'");
     }
   }
 
 
-  void OrthancConfiguration::Read(const boost::filesystem::path& configurationFile)
+  std::string OrthancConfiguration::GetConfigurationAbsolutePath() const
   {
+    if (configurationPaths_.empty())
+    {
+      return "";
+    }
+    else
+    {
+      if (configurationPaths_.size() >= 2)
+      {
+        LOG(WARNING) << "A plugin has called the deprecated OrthancPluginGetConfigurationPath() primitive, the results "
+                     << "are unreliable because multiple configuration paths were provided";
+      }
+
+      const boost::filesystem::path& first = configurationPaths_.front();
+
+      if (boost::filesystem::is_directory(first))
+      {
+        return boost::filesystem::absolute(first).parent_path().string();
+      }
+      else
+      {
+        return boost::filesystem::absolute(first).string();
+      }
+    }    
+  }
+
+
+  void OrthancConfiguration::Read(const std::list<boost::filesystem::path>& configurationPaths)
+  {
+    if (configurationPaths.empty())
+    {
+      LOG(WARNING) << "Using the default Orthanc configuration";
+    }
+
     // Read the content of the configuration
-    configurationFileArg_ = configurationFile;
-    ReadConfiguration(json_, configurationFile);
+    configurationPaths_ = configurationPaths;
+    ReadConfiguration(userConfiguration_, configurationPaths);
 
     // Adapt the paths to the configurations
     defaultDirectory_ = boost::filesystem::current_path();
-    configurationAbsolutePath_ = "";
 
-    if (configurationFile.empty())
+    if (!configurationPaths.empty())
     {
-      if (boost::filesystem::is_directory(configurationFile))
+      const boost::filesystem::path& first = configurationPaths.front();
+
+      if (boost::filesystem::is_directory(first))
       {
-        defaultDirectory_ = configurationFile;
-        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).parent_path().string();
+        defaultDirectory_ = first;
       }
       else
       {
-        defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path();
-        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string();
+        defaultDirectory_ = boost::filesystem::path(first).parent_path();
       }
     }
-    else
-    {
-#if ORTHANC_STANDALONE != 1
-      // In a non-standalone build, we use the
-      // "Resources/Configuration.json" from the Orthanc source code
 
-      boost::filesystem::path p = ORTHANC_PATH;
-      p /= "Resources";
-      p /= "Configuration.json";
-      configurationAbsolutePath_ = boost::filesystem::absolute(p).string();
-#endif
-    }
+    LOG(WARNING) << "The paths in the Orthanc configuration will be interpreted relative to folder: "
+                 << SystemToolbox::PathToUtf8(defaultDirectory_);
   }
 
 
@@ -763,13 +856,13 @@
 
     httpServer.ClearUsers();
 
-    if (!json_.isMember(REGISTERED_USERS))
+    if (!userConfiguration_.isMember(REGISTERED_USERS))
     {
       return RegisteredUsersStatus_NoConfiguration;
     }
     else
     {
-      const Json::Value& users = json_[REGISTERED_USERS];
+      const Json::Value& users = userConfiguration_[REGISTERED_USERS];
       if (users.type() != Json::objectValue)
       {
         throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users");
@@ -812,12 +905,12 @@
   {
     target.clear();
   
-    if (!json_.isMember(key))
+    if (!userConfiguration_.isMember(key))
     {
       return;
     }
 
-    const Json::Value& lst = json_[key];
+    const Json::Value& lst = userConfiguration_[key];
 
     if (lst.type() != Json::arrayValue)
     {
@@ -836,12 +929,12 @@
   {
     target.clear();
   
-    if (!json_.isMember(key))
+    if (!userConfiguration_.isMember(key))
     {
       return;
     }
 
-    const Json::Value& lst = json_[key];
+    const Json::Value& lst = userConfiguration_[key];
 
     if (lst.type() != Json::arrayValue)
     {
@@ -1014,7 +1107,7 @@
 
   void OrthancConfiguration::Format(std::string& result) const
   {
-    Toolbox::WriteStyledJson(result, json_);
+    Toolbox::WriteStyledJson(result, userConfiguration_);
   }
 
 
@@ -1024,17 +1117,17 @@
 
     // Propagate the encoding to the configuration file that is
     // stored in memory
-    json_["DefaultEncoding"] = EnumerationToString(encoding);
+    userConfiguration_["DefaultEncoding"] = EnumerationToString(encoding);
   }
 
 
   bool OrthancConfiguration::HasConfigurationChanged() const
   {
     Json::Value current;
-    ReadConfiguration(current, configurationFileArg_);
+    ReadConfiguration(current, configurationPaths_);
 
     std::string a, b;
-    Toolbox::WriteFastJson(a, json_);
+    Toolbox::WriteFastJson(a, userConfiguration_);
     Toolbox::WriteFastJson(b, current);
 
     return a != b;
@@ -1116,9 +1209,9 @@
 
     static const char* const ACCEPTED_TRANSFER_SYNTAXES = "AcceptedTransferSyntaxes";
 
-    if (json_.isMember(ACCEPTED_TRANSFER_SYNTAXES))
+    if (userConfiguration_.isMember(ACCEPTED_TRANSFER_SYNTAXES))
     {
-      ParseAcceptedTransferSyntaxes(target, json_[ACCEPTED_TRANSFER_SYNTAXES]);
+      ParseAcceptedTransferSyntaxes(target, userConfiguration_[ACCEPTED_TRANSFER_SYNTAXES]);
     }
     else
     {
@@ -1205,9 +1298,9 @@
 
   void OrthancConfiguration::LoadWarnings()
   {
-    if (json_.isMember(WARNINGS))
+    if (userConfiguration_.isMember(WARNINGS))
     {
-      const Json::Value& warnings = json_[WARNINGS];
+      const Json::Value& warnings = userConfiguration_[WARNINGS];
       if (!warnings.isObject())
       {
         throw OrthancException(ErrorCode_BadFileFormat, std::string(WARNINGS) + " configuration entry is not a Json object");
--- a/OrthancServer/Sources/OrthancConfiguration.h	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Wed Aug 12 08:33:57 2026 +0200
@@ -102,22 +102,20 @@
     typedef std::map<std::string, WebServiceParameters>       Peers;
     typedef std::map<std::string, unsigned int>               JobsEngineThreadsCount;
 
-    boost::shared_mutex      mutex_;
-    Json::Value              json_;
-    boost::filesystem::path  defaultDirectory_;
-    std::string              configurationAbsolutePath_;
-    FontRegistry             fontRegistry_;
-    boost::filesystem::path  configurationFileArg_;
-    Modalities               modalities_;
-    Peers                    peers_;
-    JobsEngineThreadsCount   jobsEngineThreadsCount_;
-    ServerIndex*             serverIndex_;
-    std::set<Warnings>       disabledWarnings_;
+    boost::shared_mutex                 mutex_;
+    Json::Value                         defaultConfiguration_;
+    std::list<boost::filesystem::path>  configurationPaths_;
+    Json::Value                         userConfiguration_;
+    boost::filesystem::path             defaultDirectory_;
+    FontRegistry                        fontRegistry_;
+    Modalities                          modalities_;
+    Peers                               peers_;
+    JobsEngineThreadsCount              jobsEngineThreadsCount_;
+    ServerIndex*                        serverIndex_;
+    std::set<Warnings>                  disabledWarnings_;
+    std::string                         orthancDicomAet_;
 
-    OrthancConfiguration() :
-      serverIndex_(NULL)
-    {
-    }
+    OrthancConfiguration();
 
     void LoadModalitiesFromJson(const Json::Value& source);
     
@@ -158,7 +156,7 @@
 
       const Json::Value& GetJson() const
       {
-        return configuration_.json_;
+        return configuration_.userConfiguration_;
       }
     };
 
@@ -188,22 +186,19 @@
 
       const Json::Value& GetJson() const
       {
-        return configuration_.json_;
+        return configuration_.userConfiguration_;
       }
     };
 
 
-    const std::string& GetConfigurationAbsolutePath() const
-    {
-      return configurationAbsolutePath_;
-    }
+    std::string GetConfigurationAbsolutePath() const;
 
     const FontRegistry& GetFontRegistry() const
     {
       return fontRegistry_;
     }
 
-    void Read(const boost::filesystem::path &configurationFile);
+    void Read(const std::list<boost::filesystem::path>& configurationPaths);
 
     // "SetServerIndex()" must have been called
     void LoadModalitiesAndPeers();
@@ -224,6 +219,9 @@
     bool LookupIntegerParameter(int& target,
                                 const std::string& parameter) const;
 
+    bool GetIntegerParameter(int& target,
+                             const std::string& parameter) const;
+
     bool LookupUnsignedIntegerParameter(unsigned int& target,
                                         const std::string& parameter) const;
 
@@ -313,10 +311,9 @@
       return disabledWarnings_.count(warning) == 0;
     }
 
-    std::string GetOrthancAET() const
-    {
-      return GetStringParameter(ORTHANC_CONFIG_DICOM_AET);
-    }
+    void LoadOrthancAET();
+
+    std::string GetOrthancAET() const;
 
     std::string GetOrthancName() const
     {
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -331,7 +331,7 @@
 
 
 
-  void OrthancInitialize(const boost::filesystem::path& configurationFile)
+  void OrthancInitialize(const std::list<boost::filesystem::path>& configurationPaths)
   {
     static const char* const LOCALE = "Locale";
     static const char* const PKCS11 = "Pkcs11";
@@ -348,7 +348,8 @@
     InitializeServerEnumerations();
 
     // Read the user-provided configuration
-    lock.GetConfiguration().Read(configurationFile);
+    lock.GetConfiguration().Read(configurationPaths);
+    lock.GetConfiguration().LoadOrthancAET();  // read it early because we need it for the ServerDatabaseIdentifier
 
     {
       std::string locale;
@@ -405,6 +406,12 @@
   }
 
 
+  void OrthancInitialize()
+  {
+    std::list<boost::filesystem::path> configurationPaths;
+    OrthancInitialize(configurationPaths);
+  }
+
 
   void OrthancFinalize()
   {
--- a/OrthancServer/Sources/OrthancInitialization.h	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.h	Wed Aug 12 08:33:57 2026 +0200
@@ -30,7 +30,9 @@
 
 namespace Orthanc
 {
-  void OrthancInitialize(const boost::filesystem::path& configurationFile);
+  void OrthancInitialize(const std::list<boost::filesystem::path>& configurationPaths);
+
+  void OrthancInitialize();  // Use only the default configuration
 
   void OrthancFinalize();
 
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -1404,7 +1404,7 @@
     // New in Orthanc 1.12.11
     value[KEY_ALLOW_UTF8] = allowUtf8_;
 
-    // New in Orthanc 1.12.12
+    // New in Orthanc 1.13.0
     if (hasLossyQuality_)
     {
       value[KEY_LOSSY_QUALITY] = lossyQuality_;
--- a/OrthancServer/Sources/main.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancServer/Sources/main.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -57,6 +57,8 @@
 #include "StorageCommitmentReports.h"
 
 #include <boost/algorithm/string/predicate.hpp>
+#include <boost/program_options.hpp>
+
 #include <dcmtk/dcmnet/dimse.h>  // For STATUS_STORE_Error_CannotUnderstand
 
 #if defined(_WIN32) || defined(__CYGWIN__)
@@ -727,13 +729,14 @@
 };
 
 
-static void PrintHelp(const boost::filesystem::path& path)
+static void PrintHelp(const boost::filesystem::path& path,
+                      const boost::program_options::options_description& all)
 {
   std::cout 
     << "Usage: " << SystemToolbox::PathToUtf8(path) << " [OPTION]... [CONFIGURATION]" << std::endl
     << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl
     << std::endl
-    << "The \"CONFIGURATION\" argument can be a single file or a directory. In the " << std::endl
+    << "The \"CONFIGURATION\" argument can be a list of files or directories. In the " << std::endl
     << "case of a directory, all the JSON files it contains will be merged. " << std::endl
     << "If no configuration path is given on the command line, a set of default " << std::endl
     << "parameters is used. Please refer to the Orthanc Book for the full " << std::endl
@@ -744,43 +747,9 @@
     << "reset the value of other log-related options that were read before." << std::endl
     << std::endl
     << "The recommended set of options to debug DICOM communications is " << std::endl
-    << "\"--verbose --trace-dicom --logfile=dicom.log\"" << std::endl
-    << std::endl
-    << "Command-line options:" << std::endl
-    << "  --help\t\tdisplay this help and exit" << std::endl
-    << "  --logdir=[dir]\tdirectory where to store the log files" << std::endl
-    << "\t\t\t(by default, the log is dumped to stderr)" << std::endl
-    << "  --logfile=[file]\tfile where to store the log of Orthanc" << std::endl
-    << "\t\t\t(by default, the log is dumped to stderr)" << std::endl
-    << "  --config=[file]\tcreate a sample configuration file and exit" << std::endl
-    << "\t\t\t(if \"file\" is \"-\", dumps to stdout)" << std::endl
-    << "  --errors\t\tprint the supported error codes and exit" << std::endl
-    << "  --verbose\t\tbe verbose in logs" << std::endl
-    << "  --logs-no-thread\tto remove thread names from logs" << std::endl
-    << "  --logs-no-context\tto remove contexts from logs" << std::endl
-    << "  --logs-threadnames-in-context\t\tto add caller thread names in logs contexts" << std::endl
-    << "  --trace\t\thighest verbosity in logs (for debug)" << std::endl
-    << "  --upgrade\t\tallow Orthanc to upgrade the version of the" << std::endl
-    << "\t\t\tdatabase (beware that the database will become" << std::endl
-    << "\t\t\tincompatible with former versions of Orthanc)" << std::endl
-    << "  --no-jobs\t\tdon't restart the jobs that were stored during" << std::endl
-    << "\t\t\tthe last execution of Orthanc" << std::endl
-    << "  --openapi=[file]\twrite the OpenAPI documentation and exit" << std::endl
-    << "\t\t\t(if \"file\" is \"-\", dumps to stdout)" << std::endl
-    << "  --cheatsheet=[file]\twrite the cheat sheet of REST API as CSV" << std::endl
-    << "\t\t\tand exit (if \"file\" is \"-\", dumps to stdout)" << std::endl
-    << "  --version\t\toutput version information and exit" << std::endl
-    << std::endl
-    << "Fine-tuning of log categories:" << std::endl;
+    << "\"--verbose --trace-dicom --logfile=dicom.log\"" << std::endl;
 
-  for (size_t i = 0; i < Logging::GetCategoriesCount(); i++)
-  {
-    const std::string name = Logging::GetCategoryName(i);
-    std::cout << "  --verbose-" << name
-              << "\tbe verbose in logs of category \"" << name << "\"" << std::endl;
-    std::cout << "  --trace-" << name
-              << "\tuse highest verbosity for logs of category \"" << name << "\"" << std::endl;
-  }
+  std::cout << all;
   
   std::cout
     << std::endl
@@ -1731,7 +1700,13 @@
 
     if (context.IsReadOnly())
     {
-      LOG(WARNING) << "READ-ONLY SYSTEM: ignoring these configurations: " << ORTHANC_CONFIG_STORAGE_COMPRESSION << ", " << ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS << ", " << ORTHANC_CONFIG_OVERWRITE_INSTANCES << ", " << ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT << ", " << ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE <<", " << ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE << ", SaveJobs"; 
+      LOG(WARNING) << "READ-ONLY SYSTEM: ignoring these configurations: "
+                   << ORTHANC_CONFIG_STORAGE_COMPRESSION << ", "
+                   << ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS << ", "
+                   << ORTHANC_CONFIG_OVERWRITE_INSTANCES << ", "
+                   << ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT << ", "
+                   << ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE << ", "
+                   << ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE << ", SaveJobs";
     }
     else
     {
@@ -1750,7 +1725,9 @@
           overwriteInstancesMode = StringToOverwriteInstancesMode(strOverwriteInstancesMode);
           if (overwriteInstancesMode == OverwriteInstancesMode_IfChanged && !context.IsStoreMD5ForAttachments())
           {
-            LOG(ERROR) << "Can not set \"" << ORTHANC_CONFIG_OVERWRITE_INSTANCES << "\" to \"IfChanged\" when \"" << ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS << "\" is set to false.";
+            LOG(ERROR) << "Can not set \"" << ORTHANC_CONFIG_OVERWRITE_INSTANCES
+                       << "\" to \"IfChanged\" when \"" << ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS
+                       << "\" is set to false.";
             return false;
           }
         }
@@ -1982,6 +1959,128 @@
 }
 
 
+static int ExportOpenApi(const std::string& target)
+{
+  Json::Value openapi;
+
+  {
+    SQLiteDatabaseWrapper inMemoryDatabase;
+    inMemoryDatabase.Open();
+    PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea);
+    ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */,
+                          true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
+    OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
+    restApi.GenerateOpenApiDocumentation(openapi);
+    context.Stop();
+  }
+
+  openapi["info"]["version"] = ORTHANC_VERSION;
+  openapi["info"]["title"] = "Orthanc API";
+  openapi["info"]["description"] =
+    "This is the full documentation of the [REST API](https://orthanc.uclouvain.be/book/users/rest.html) "
+    "of Orthanc.<p>This reference is automatically generated from the source code of Orthanc. A "
+    "[shorter cheat sheet](https://orthanc.uclouvain.be/book/users/rest-cheatsheet.html) is part of "
+    "the Orthanc Book.<p>An earlier, manually crafted version from August 2019, is [still available]"
+    "(2019-08-orthanc-openapi.html), but is not up-to-date anymore ([source]"
+    "(https://groups.google.com/g/orthanc-users/c/NUiJTEICSl8/m/xKeqMrbqAAAJ)).";
+
+  Json::Value server = Json::objectValue;
+  server["url"] = "https://orthanc.uclouvain.be/demo/";
+  openapi["servers"].append(server);
+
+  std::string s;
+  Toolbox::WriteStyledJson(s, openapi);
+
+  try
+  {
+    if (target == "-")
+    {
+      std::cout << s;   // Print to stdout
+    }
+    else
+    {
+      SystemToolbox::WriteFile(s, SystemToolbox::PathFromUtf8(target));
+    }
+
+    return 0;
+  }
+  catch (OrthancException&)
+  {
+    LOG(ERROR) << "Cannot export OpenAPI documentation as file \"" << target << "\"";
+    return -1;
+  }
+}
+
+
+static int ExportCheatSheet(const std::string& target)
+{
+  std::string cheatsheet;
+
+  {
+    SQLiteDatabaseWrapper inMemoryDatabase;
+    inMemoryDatabase.Open();
+    PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea);
+    ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */,
+                          true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
+    OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
+    restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html");
+    context.Stop();
+  }
+
+  try
+  {
+    if (target == "-")
+    {
+      std::cout << cheatsheet;   // Print to stdout
+    }
+    else
+    {
+      SystemToolbox::WriteFile(cheatsheet, SystemToolbox::PathFromUtf8(target));
+    }
+
+    return 0;
+  }
+  catch (OrthancException&)
+  {
+    LOG(ERROR) << "Cannot export REST cheat sheet as file \"" << target << "\"";
+    return -1;
+  }
+}
+
+
+static int ExportResource(const std::string& target,
+                          Orthanc::ServerResources::FileResourceId resource)
+{
+  try
+  {
+    std::string content;
+    GetFileResource(content, resource);
+
+#if defined(_WIN32)
+    // Replace UNIX newlines with DOS newlines
+    boost::replace_all(content, "\n", "\r\n");
+#endif
+
+    if (target == "-")
+    {
+      // New in 1.5.8: Print to stdout
+      std::cout << content;
+    }
+    else
+    {
+      SystemToolbox::WriteFile(content, SystemToolbox::PathFromUtf8(target));
+    }
+
+    return 0;
+  }
+  catch (OrthancException&)
+  {
+    LOG(ERROR) << "Cannot write to file " << SystemToolbox::PathFromUtf8(target) << ", aborting.";
+    return -1;
+  }
+}
+
+
 #if defined(_WIN32) && !defined(__MINGW32__)
 // arguments are passed as UTF-16 on Windows
 int wmain(int argc, wchar_t *argv[])
@@ -2016,248 +2115,306 @@
 
   bool upgradeDatabase = false;
   bool loadJobsFromDatabase = true;
-  boost::filesystem::path configurationFile;
+  std::list<boost::filesystem::path> configurationPaths;
+
 
   /**
    * Parse the command-line options.
-   **/ 
+   **/
+
+  static const char* const OPTION_HELP = "help";
+  static const char* const OPTION_VERSION = "version";
+  static const char* const OPTION_CONFIG = "config";
+  static const char* const OPTION_ADVANCED_CONFIG = "advanced-config";
+  static const char* const OPTION_ERRORS = "errors";
+  static const char* const OPTION_UPGRADE = "upgrade";
+  static const char* const OPTION_NO_JOBS = "no-jobs";
+  static const char* const OPTION_OPENAPI = "openapi";
+  static const char* const OPTION_CHEATSHEET = "cheatsheet";
+  static const char* const OPTION_INPUTS = "inputs";
+
+  static const char* const OPTION_LOGDIR = "logdir";
+  static const char* const OPTION_LOGFILE = "logfile";
+  static const char* const OPTION_VERBOSE = "verbose";
+  static const char* const OPTION_TRACE = "trace";
+  static const char* const OPTION_LOGS_NO_THREAD = "logs-no-thread";
+  static const char* const OPTION_LOGS_NO_CONTEXT = "logs-no-context";
+  static const char* const OPTION_LOGS_THREAD_NAMES_IN_CONTEXT = "logs-thread-names-in-context";
+
+  boost::program_options::options_description allWithoutHidden;
+  std::vector<std::string> orderSensitiveArguments;
+  std::vector<std::string> unrecognized;
+  boost::program_options::variables_map options;
 
-  for (size_t i = 1; i < arguments.size(); i++)
   {
-    const std::string& argument = arguments[i];    
+    boost::program_options::options_description generic("Generic options");
+    generic.add_options()
+      (OPTION_HELP, "display this help and exit")
+      (OPTION_VERSION, "output version information and exit")
+      (OPTION_CONFIG, boost::program_options::value<std::string>()->value_name("file"),
+       "create a sample configuration file and exit (if \"file\" is \"-\", dumps to stdout)")
+      (OPTION_ADVANCED_CONFIG, boost::program_options::value<std::string>()->value_name("file"),
+       "create a file containing the advanced configuration options and exit (if \"file\" is \"-\", dumps to stdout)")
+      (OPTION_ERRORS, "print the supported error codes and exit")
+      (OPTION_UPGRADE, "allow Orthanc to upgrade the version of the database "
+       "(beware that the database will become incompatible with former versions of Orthanc)")
+      (OPTION_NO_JOBS, "don't restart the jobs that were stored during the last execution of Orthanc")
+      (OPTION_OPENAPI, boost::program_options::value<std::string>()->value_name("file"),
+       "write the OpenAPI documentation and exit (if \"file\" is \"-\", dumps to stdout)")
+      (OPTION_CHEATSHEET, boost::program_options::value<std::string>()->value_name("file"),
+       "write the cheat sheet of REST API as CSV and exit (if \"file\" is \"-\", dumps to stdout)");
+
+    boost::program_options::options_description logging("Logging options");
+    logging.add_options()
+      (OPTION_LOGDIR, boost::program_options::value<std::string>()->value_name("dir"),
+       "directory where to store the log files (by default, the log is dumped to stderr)")
+      (OPTION_LOGFILE, boost::program_options::value<std::string>()->value_name("file"),
+       "file where to store the log of Orthanc (by default, the log is dumped to stderr)")
+      (OPTION_VERBOSE, "be verbose in logs")
+      (OPTION_TRACE, "highest verbosity in logs (for debug)")
+
+      // New in Orthanc 1.12.12
+      (OPTION_LOGS_NO_THREAD, "remove thread names from logs")
 
-    if (argument.empty())
+      // New in Orthanc 1.13.0
+      (OPTION_LOGS_NO_CONTEXT, "remove contexts from logs")
+      (OPTION_LOGS_THREAD_NAMES_IN_CONTEXT, "include caller thread names in log contexts");
+
+    boost::program_options::options_description finetuning("Fine-tuning of log categories");
+
+    std::set<std::string> orderSensitiveOptions;
+    orderSensitiveOptions.insert(OPTION_VERBOSE);
+    orderSensitiveOptions.insert(OPTION_TRACE);
+
+    for (size_t i = 0; i < Logging::GetCategoriesCount(); i++)
     {
-      // Ignore empty arguments
+      const std::string name = Logging::GetCategoryName(i);
+      const std::string verboseName = "verbose-" + name;
+      const std::string verboseDescription = "be verbose in logs of category \"" + name + "\"";
+      const std::string traceName = "trace-" + name;
+      const std::string traceDescription = "use highest verbosity for logs of category \"" + name + "\"";
+
+      finetuning.add_options() (verboseName.c_str(), verboseDescription.c_str());
+      finetuning.add_options() (traceName.c_str(), traceDescription.c_str());
+
+      orderSensitiveOptions.insert(verboseName);
+      orderSensitiveOptions.insert(traceName);
     }
-    else if (argument[0] != '-')
+
+    allWithoutHidden.add(generic);
+    allWithoutHidden.add(logging);
+    allWithoutHidden.add(finetuning);
+
+    boost::program_options::options_description hidden;
+    hidden.add_options()
+      (OPTION_INPUTS, boost::program_options::value< std::vector<std::string> >(),
+       "Input configuration files");
+
+    boost::program_options::options_description all;
+    all.add(allWithoutHidden);
+    all.add(hidden);
+
+    boost::program_options::positional_options_description positional;
+    positional.add(OPTION_INPUTS, -1);  // -1 = all remaining positional arguments
+
+    if (arguments.empty())
     {
-      if (!configurationFile.empty())
+      LOG(ERROR) << "The program name is not available, aborting";
+      return -1;
+    }
+
+    std::vector<std::string> orderNonSensitiveArguments;
+    orderSensitiveArguments.reserve(arguments.size());
+    orderNonSensitiveArguments.reserve(arguments.size());
+
+    for (size_t i = 1 /* skip the program name */; i < arguments.size(); i++)
+    {
+      bool isOrderSensitive = false;
+
+      if (boost::starts_with(arguments[i], "--"))
       {
-        LOG(ERROR) << "More than one configuration path were provided on the command line, aborting";
-        return -1;
-      }
-      else
-      {
-        // Use the first argument that does not start with a "-" as
-        // the configuration file
+        const std::string key = arguments[i].substr(2);
 
-        configurationFile = SystemToolbox::PathFromUtf8(argument);
-//        // TODO WHAT IS THE ENCODING?
-//#if defined(_WIN32)
-//        //configurationFileUtf8Str = SystemToolbox::WStringToUtf8(SystemToolbox::WStringFromCharPtr(argv[i]));
-//#else
-//        configurationFileUtf8Str = std::string(argv[i]);
-//#endif
+        if (orderSensitiveOptions.find(key) != orderSensitiveOptions.end())
+        {
+          isOrderSensitive = true;
+          orderSensitiveArguments.push_back(key);
+        }
+      }
+
+      if (!isOrderSensitive)
+      {
+        orderNonSensitiveArguments.push_back(arguments[i]);
       }
     }
-    else if (argument == "--errors")
+
+    try
     {
-      PrintErrors(SystemToolbox::PathFromUtf8(arguments[0]));
-      return 0;
+      boost::program_options::parsed_options parsed = boost::program_options::command_line_parser(orderNonSensitiveArguments)
+        .options(all)
+        .positional(positional)
+        .allow_unregistered()   // don't throw on unknown options
+        .run();
+
+      // Pull out everything that didn't match a declared option
+      unrecognized = boost::program_options::collect_unrecognized(parsed.options, boost::program_options::exclude_positional);
+
+      boost::program_options::store(parsed, options);
+      boost::program_options::notify(options);
     }
-    else if (argument == "--help")
+    catch (boost::program_options::error& e)
     {
-      PrintHelp(SystemToolbox::PathFromUtf8(arguments[0]));
-      return 0;
+      LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what();
+      return -1;
     }
-    else if (argument == "--version")
-    {
-      PrintVersion(SystemToolbox::PathFromUtf8(arguments[0]));
-      return 0;
-    }
-    else if (argument == "--verbose")
+  }
+
+
+  /**
+   * Process the command-line options.
+   **/
+
+  for (size_t i = 0; i < unrecognized.size(); i++)
+  {
+    LOG(WARNING) << "Option unsupported by the core of Orthanc: " << unrecognized[i];
+  }
+
+  for (size_t i = 0; i < orderSensitiveArguments.size(); i++)
+  {
+    const std::string& arg = orderSensitiveArguments[i];
+
+    if (arg == OPTION_VERBOSE)
     {
       SetGlobalVerbosity(Verbosity_Verbose);
     }
-    else if (argument == "--logs-no-thread")
-    {
-      Logging::SetThreadNamesEnabled(false);
-    }
-    else if (argument == "--logs-no-context")
-    {
-      Logging::SetThreadContextsEnabled(false);
-    }
-    else if (argument == "--logs-threadnames-in-context")
-    {
-      Logging::SetThreadNamesInContextsEnabled(true);
-    }
-    else if (argument == "--trace")
+    else if (arg == OPTION_TRACE)
     {
       SetGlobalVerbosity(Verbosity_Trace);
     }
-    else if (boost::starts_with(argument, "--verbose-") &&
-             SetCategoryVerbosity(Verbosity_Verbose, argument.substr(10)))
-    {
-      // New in Orthanc 1.8.1
-    }
-    else if (boost::starts_with(argument, "--trace-") &&
-             SetCategoryVerbosity(Verbosity_Trace, argument.substr(8)))
+    else if (boost::starts_with(arg, "verbose-") &&
+             SetCategoryVerbosity(Verbosity_Verbose, arg.substr(arg.find('-') + 1)))
     {
       // New in Orthanc 1.8.1
     }
-    else if (boost::starts_with(argument, "--logdir="))
-    {
-      // TODO WHAT IS THE ENCODING?
-      const std::string directory = argument.substr(9);
-
-      try
-      {
-        Logging::SetTargetFolder(directory);
-      }
-      catch (OrthancException&)
-      {
-        LOG(ERROR) << "The directory where to store the log files (" 
-                   << directory << ") is inexistent, aborting.";
-        return -1;
-      }
-    }
-    else if (boost::starts_with(argument, "--logfile="))
-    {
-      // TODO WHAT IS THE ENCODING?
-      const std::string file = argument.substr(10);
-
-      try
-      {
-        Logging::SetTargetFile(file);
-      }
-      catch (OrthancException&)
-      {
-        LOG(ERROR) << "Cannot write to the specified log file (" 
-                   << file << "), aborting.";
-        return -1;
-      }
-    }
-    else if (argument == "--upgrade")
-    {
-      upgradeDatabase = true;
-    }
-    else if (argument == "--no-jobs")
-    {
-      loadJobsFromDatabase = false;
-    }
-    else if (boost::starts_with(argument, "--config="))
-    {
-      // TODO WHAT IS THE ENCODING?
-      std::string configurationSample;
-      GetFileResource(configurationSample, ServerResources::CONFIGURATION_SAMPLE);
-
-#if defined(_WIN32)
-      // Replace UNIX newlines with DOS newlines 
-      boost::replace_all(configurationSample, "\n", "\r\n");
-#endif
-
-      std::string target = argument.substr(9);
-
-      try
-      {
-        if (target == "-")
-        {
-          // New in 1.5.8: Print to stdout
-          std::cout << configurationSample;
-        }
-        else
-        {
-          SystemToolbox::WriteFile(configurationSample, SystemToolbox::PathFromUtf8(target));
-        }
-        return 0;
-      }
-      catch (OrthancException&)
-      {
-        LOG(ERROR) << "Cannot write sample configuration as file \"" << target << "\"";
-        return -1;
-      }
-    }
-    else if (boost::starts_with(argument, "--openapi="))
+    else if (boost::starts_with(arg, "trace-") &&
+             SetCategoryVerbosity(Verbosity_Trace, arg.substr(arg.find('-') + 1)))
     {
-      std::string target = argument.substr(10);
-
-      try
-      {
-        Json::Value openapi;
-
-        {
-          SQLiteDatabaseWrapper inMemoryDatabase;
-          inMemoryDatabase.Open();
-          PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea);
-          ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */,
-                                true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
-          OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
-          restApi.GenerateOpenApiDocumentation(openapi);
-          context.Stop();
-        }
-
-        openapi["info"]["version"] = ORTHANC_VERSION;
-        openapi["info"]["title"] = "Orthanc API";
-        openapi["info"]["description"] =
-          "This is the full documentation of the [REST API](https://orthanc.uclouvain.be/book/users/rest.html) "
-          "of Orthanc.<p>This reference is automatically generated from the source code of Orthanc. A "
-          "[shorter cheat sheet](https://orthanc.uclouvain.be/book/users/rest-cheatsheet.html) is part of "
-          "the Orthanc Book.<p>An earlier, manually crafted version from August 2019, is [still available]"
-          "(2019-08-orthanc-openapi.html), but is not up-to-date anymore ([source]"
-          "(https://groups.google.com/g/orthanc-users/c/NUiJTEICSl8/m/xKeqMrbqAAAJ)).";
-
-        Json::Value server = Json::objectValue;
-        server["url"] = "https://orthanc.uclouvain.be/demo/";
-        openapi["servers"].append(server);
-        
-        std::string s;
-        Toolbox::WriteStyledJson(s, openapi);
-
-        if (target == "-")
-        {
-          std::cout << s;   // Print to stdout
-        }
-        else
-        {
-          SystemToolbox::WriteFile(s, SystemToolbox::PathFromUtf8(target));
-        }
-        return 0;
-      }
-      catch (OrthancException&)
-      {
-        LOG(ERROR) << "Cannot export OpenAPI documentation as file \"" << target << "\"";
-        return -1;
-      }
-    }
-    else if (boost::starts_with(argument, "--cheatsheet="))
-    {
-      std::string target = argument.substr(13);
-
-      try
-      {
-        std::string cheatsheet;
-
-        {
-          SQLiteDatabaseWrapper inMemoryDatabase;
-          inMemoryDatabase.Open();
-          PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea);
-          ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */,
-                                true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
-          OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
-          restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html");
-          context.Stop();
-        }
-
-        if (target == "-")
-        {
-          std::cout << cheatsheet;   // Print to stdout
-        }
-        else
-        {
-          SystemToolbox::WriteFile(cheatsheet, SystemToolbox::PathFromUtf8(target));
-        }
-        return 0;
-      }
-      catch (OrthancException&)
-      {
-        LOG(ERROR) << "Cannot export REST cheat sheet as file \"" << target << "\"";
-        return -1;
-      }
+      // New in Orthanc 1.8.1
     }
     else
     {
-      LOG(WARNING) << "Option unsupported by the core of Orthanc: " << argument;
+      // Should never happen
+      LOG(ERROR) << "Unsupported command-line option: " << arg;
+      return -1;
+    }
+  }
+
+  if (options.count(OPTION_HELP) == 1)
+  {
+    PrintHelp(arguments[0], allWithoutHidden);
+    return 0;
+  }
+
+  if (options.count(OPTION_VERSION) == 1)
+  {
+    PrintVersion(arguments[0]);
+    return 0;
+  }
+
+  if (options.count(OPTION_LOGDIR) == 1)
+  {
+    const std::string directory = options[OPTION_LOGDIR].as<std::string>();
+
+    try
+    {
+      Logging::SetTargetFolder(directory);
+    }
+    catch (OrthancException&)
+    {
+      LOG(ERROR) << "The directory where to store the log files ("
+                 << SystemToolbox::PathFromUtf8(directory) << ") is inexistent, aborting.";
+      return -1;
+    }
+  }
+
+  if (options.count(OPTION_LOGFILE) == 1)
+  {
+    const std::string file = options[OPTION_LOGFILE].as<std::string>();
+
+    try
+    {
+      Logging::SetTargetFile(file);
+    }
+    catch (OrthancException&)
+    {
+      LOG(ERROR) << "Cannot write to the specified log file ("
+                 << SystemToolbox::PathFromUtf8(file) << "), aborting.";
+      return -1;
+    }
+  }
+
+  if (options.count(OPTION_CONFIG) == 1)
+  {
+    const std::string file = options[OPTION_CONFIG].as<std::string>();
+    return ExportResource(file, Orthanc::ServerResources::CONFIGURATION_SAMPLE);
+  }
+
+  if (options.count(OPTION_ADVANCED_CONFIG) == 1)
+  {
+    const std::string file = options[OPTION_ADVANCED_CONFIG].as<std::string>();
+    return ExportResource(file, Orthanc::ServerResources::ADVANCED_CONFIGURATION_SAMPLE);
+  }
+
+  if (options.count(OPTION_ERRORS) == 1)
+  {
+    PrintErrors(arguments[0]);
+    return 0;
+  }
+
+  if (options.count(OPTION_UPGRADE) == 1)
+  {
+    upgradeDatabase = true;
+  }
+
+  if (options.count(OPTION_NO_JOBS) == 1)
+  {
+    loadJobsFromDatabase = false;
+  }
+
+  if (options.count(OPTION_OPENAPI) == 1)
+  {
+    const std::string file = options[OPTION_OPENAPI].as<std::string>();
+    return ExportOpenApi(file);
+  }
+
+  if (options.count(OPTION_CHEATSHEET) == 1)
+  {
+    const std::string file = options[OPTION_CHEATSHEET].as<std::string>();
+    return ExportCheatSheet(file);
+  }
+
+  if (options.count(OPTION_LOGS_NO_THREAD) == 1)
+  {
+    Logging::SetThreadNamesEnabled(false);
+  }
+
+  if (options.count(OPTION_LOGS_NO_CONTEXT) == 1)
+  {
+    Logging::SetThreadContextsEnabled(false);
+  }
+
+  if (options.count(OPTION_LOGS_THREAD_NAMES_IN_CONTEXT) == 1)
+  {
+    Logging::SetThreadNamesInContextsEnabled(true);
+  }
+
+  if (options.count(OPTION_INPUTS) != 0)
+  {
+    const std::vector<std::string>& inputs = options[OPTION_INPUTS].as<std::vector<std::string> >();
+
+    for (size_t i = 0; i < inputs.size(); i++)
+    {
+      configurationPaths.push_back(SystemToolbox::PathFromUtf8(inputs[i]));
     }
   }
 
@@ -2323,7 +2480,7 @@
   {
     for (;;)
     {
-      OrthancInitialize(configurationFile);
+      OrthancInitialize(configurationPaths);
 
       bool restart = StartOrthanc(arguments, upgradeDatabase, loadJobsFromDatabase);
       if (restart)
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Mon Aug 10 14:59:05 2026 +0200
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Wed Aug 12 08:33:57 2026 +0200
@@ -548,7 +548,7 @@
   SetGlobalVerbosity(Verbosity_Verbose);
   Toolbox::DetectEndianness();
   SystemToolbox::MakeDirectory(SystemToolbox::PathFromUtf8("UnitTestsResults"));
-  OrthancInitialize("");
+  OrthancInitialize();
 
   ::testing::InitGoogleTest(&argc, argv);
   int result = RUN_ALL_TESTS();