changeset 6620:e46d709aa0ca

new experimental config PatientLevelEnabled
author Alain Mazy <am@orthanc.team>
date Mon, 16 Mar 2026 12:55:36 +0100
parents 94ce05ebc75c
children 7c6c02b669a8
files NEWS OrthancServer/Resources/Configuration.json OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp OrthancServer/Sources/main.cpp
diffstat 10 files changed, 115 insertions(+), 24 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Mar 16 10:47:50 2026 +0100
+++ b/NEWS	Mon Mar 16 12:55:36 2026 +0100
@@ -4,6 +4,8 @@
 General
 -------
 
+* New experimental configuration "PatientLevelEnabled" (TODO: work in progree)
+
 
 REST API
 --------
--- a/OrthancServer/Resources/Configuration.json	Mon Mar 16 10:47:50 2026 +0100
+++ b/OrthancServer/Resources/Configuration.json	Mon Mar 16 12:55:36 2026 +0100
@@ -1106,5 +1106,14 @@
   // Orthanc <= 1.12.10. This default value can be overwritten per archive,
   // by providing the "Utf8" field to the "{...}/archive" and
   // "/tools/create-archives" routes. (new in Orthanc 1.12.11)
-  "ZipUseUtf8" : false
+  "ZipUseUtf8" : false,
+
+  // When set to false, this option disables all /patients routes and
+  // disables patients related sanity checks when performing resource
+  // modification.  This is required e.g when your Orthanc stores
+  // resources from multiple sources in which the same PatientID might
+  // be attached to different patients because there is no centralized
+  // patient id management. (new in Orthanc 1.12.11)
+  // As of 1.12.11, this is an experimental configuration.
+  "PatientLevelEnabled": true
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon Mar 16 10:47:50 2026 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon Mar 16 12:55:36 2026 +0100
@@ -1306,18 +1306,22 @@
     Register("/instances/{id}/modify", ModifyInstance);
     Register("/series/{id}/modify", ModifyResource<ResourceType_Series>);
     Register("/studies/{id}/modify", ModifyResource<ResourceType_Study>);
-    Register("/patients/{id}/modify", ModifyResource<ResourceType_Patient>);
     Register("/tools/bulk-modify", BulkModify);
 
     Register("/instances/{id}/anonymize", AnonymizeInstance);
     Register("/series/{id}/anonymize", AnonymizeResource<ResourceType_Series>);
     Register("/studies/{id}/anonymize", AnonymizeResource<ResourceType_Study>);
-    Register("/patients/{id}/anonymize", AnonymizeResource<ResourceType_Patient>);
     Register("/tools/bulk-anonymize", BulkAnonymize);
 
     Register("/tools/create-dicom", CreateDicom);
 
     Register("/studies/{id}/split", SplitStudy);
     Register("/studies/{id}/merge", MergeStudy);
+
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients/{id}/modify", ModifyResource<ResourceType_Patient>);
+      Register("/patients/{id}/anonymize", AnonymizeResource<ResourceType_Patient>);
+    }
   }
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Mon Mar 16 10:47:50 2026 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Mon Mar 16 12:55:36 2026 +0100
@@ -862,10 +862,14 @@
     
   void OrthancRestApi::RegisterArchive()
   {
-    Register("/patients/{id}/archive", CreateSingleGet<ResourceType_Patient, false /* ZIP */>);
-    Register("/patients/{id}/archive", CreateSinglePost<ResourceType_Patient, false /* ZIP */>);
-    Register("/patients/{id}/media",   CreateSingleGet<ResourceType_Patient, true /* media */>);
-    Register("/patients/{id}/media",   CreateSinglePost<ResourceType_Patient, true /* media */>);
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients/{id}/archive", CreateSingleGet<ResourceType_Patient, false /* ZIP */>);
+      Register("/patients/{id}/archive", CreateSinglePost<ResourceType_Patient, false /* ZIP */>);
+      Register("/patients/{id}/media",   CreateSingleGet<ResourceType_Patient, true /* media */>);
+      Register("/patients/{id}/media",   CreateSinglePost<ResourceType_Patient, true /* media */>);
+    }
+
     Register("/series/{id}/archive",   CreateSingleGet<ResourceType_Series, false /* ZIP */>);
     Register("/series/{id}/archive",   CreateSinglePost<ResourceType_Series, false /* ZIP */>);
     Register("/series/{id}/media",     CreateSingleGet<ResourceType_Series, true /* media */>);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Mar 16 10:47:50 2026 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Mar 16 12:55:36 2026 +0100
@@ -4283,14 +4283,20 @@
   void OrthancRestApi::RegisterResources()
   {
     Register("/instances", ListResources<ResourceType_Instance>);
-    Register("/patients", ListResources<ResourceType_Patient>);
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients", ListResources<ResourceType_Patient>);
+    }
     Register("/series", ListResources<ResourceType_Series>);
     Register("/studies", ListResources<ResourceType_Study>);
 
     if (!context_.IsReadOnly())
     {
       Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
-      Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
+      if (context_.IsPatientLevelEnabled())
+      {
+        Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
+      }
       Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
       Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
 
@@ -4302,21 +4308,33 @@
     }
 
     Register("/instances/{id}", GetSingleResource<ResourceType_Instance>);
-    Register("/patients/{id}", GetSingleResource<ResourceType_Patient>);
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients/{id}", GetSingleResource<ResourceType_Patient>);
+    }
     Register("/series/{id}", GetSingleResource<ResourceType_Series>);
     Register("/studies/{id}", GetSingleResource<ResourceType_Study>);
 
     Register("/instances/{id}/statistics", GetResourceStatistics);
-    Register("/patients/{id}/statistics", GetResourceStatistics);
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients/{id}/statistics", GetResourceStatistics);
+    }
     Register("/studies/{id}/statistics", GetResourceStatistics);
     Register("/series/{id}/statistics", GetResourceStatistics);
 
-    Register("/patients/{id}/shared-tags", GetSharedTags);
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients/{id}/shared-tags", GetSharedTags);
+    }
     Register("/series/{id}/shared-tags", GetSharedTags);
     Register("/studies/{id}/shared-tags", GetSharedTags);
 
     Register("/instances/{id}/module", GetModule<ResourceType_Instance, DicomModule_Instance>);
-    Register("/patients/{id}/module", GetModule<ResourceType_Patient, DicomModule_Patient>);
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients/{id}/module", GetModule<ResourceType_Patient, DicomModule_Patient>);
+    }
     Register("/series/{id}/module", GetModule<ResourceType_Series, DicomModule_Series>);
     Register("/studies/{id}/module", GetModule<ResourceType_Study, DicomModule_Study>);
     Register("/studies/{id}/module-patient", GetModule<ResourceType_Study, DicomModule_Patient>);
@@ -4347,11 +4365,17 @@
     Register("/instances/{id}/header", GetInstanceHeader);
     Register("/instances/{id}/numpy", GetNumpyInstance);  // New in Orthanc 1.10.0
 
-    Register("/patients/{id}/protected", IsProtectedPatient);
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients/{id}/protected", IsProtectedPatient);
+    }
   
     if (!context_.IsReadOnly())
     {
-      Register("/patients/{id}/protected", SetPatientProtection);
+      if (context_.IsPatientLevelEnabled())
+      {
+        Register("/patients/{id}/protected", SetPatientProtection);
+      }
     }
     else
     {
@@ -4367,6 +4391,11 @@
 
     for (size_t i = 0; i < resourceTypes.size(); i++)
     {
+      if (resourceTypes[i] == "patients" && !context_.IsPatientLevelEnabled())
+      {
+        continue;
+      }
+
       Register("/" + resourceTypes[i] + "/{id}/metadata", ListMetadata);
       Register("/" + resourceTypes[i] + "/{id}/metadata/{name}", DeleteMetadata);
       Register("/" + resourceTypes[i] + "/{id}/metadata/{name}", GetMetadata);
@@ -4418,9 +4447,12 @@
     Register("/tools/find", Find<FindType_Find>);
     Register("/tools/count-resources", Find<FindType_Count>);
 
-    Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>);
-    Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>);
-    Register("/patients/{id}/instances", GetChildResources<ResourceType_Patient, ResourceType_Instance>);
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>);
+      Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>);
+      Register("/patients/{id}/instances", GetChildResources<ResourceType_Patient, ResourceType_Instance>);
+    }
     Register("/studies/{id}/series", GetChildResources<ResourceType_Study, ResourceType_Series>);
     Register("/studies/{id}/instances", GetChildResources<ResourceType_Study, ResourceType_Instance>);
     Register("/series/{id}/instances", GetChildResources<ResourceType_Series, ResourceType_Instance>);
@@ -4432,7 +4464,10 @@
     Register("/instances/{id}/study", GetParentResource<ResourceType_Instance, ResourceType_Study>);
     Register("/instances/{id}/series", GetParentResource<ResourceType_Instance, ResourceType_Series>);
 
-    Register("/patients/{id}/instances-tags", GetChildInstancesTags);
+    if (context_.IsPatientLevelEnabled())
+    {
+      Register("/patients/{id}/instances-tags", GetChildInstancesTags);
+    }
     Register("/studies/{id}/instances-tags", GetChildInstancesTags);
     Register("/series/{id}/instances-tags", GetChildInstancesTags);
 
@@ -4443,7 +4478,10 @@
 
     if (!context_.IsReadOnly())
     {
-      Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
+      if (context_.IsPatientLevelEnabled())
+      {
+        Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
+      }
       Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
       Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
       Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Mar 16 10:47:50 2026 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Mar 16 12:55:36 2026 +0100
@@ -100,6 +100,7 @@
     static const char* const HAS_EXTENDED_FIND = "HasExtendedFind";
     static const char* const READ_ONLY = "ReadOnly";
     static const char* const HAS_RESERVE_QUEUE_VALUE = "HasReserveQueueValue";
+    static const char* const PATIENT_LEVEL_ENABLED = "PatientLevelEnabled";
 
     if (call.IsDocumentation())
     {
@@ -153,6 +154,8 @@
                         "and 'HasReserveQueueValue' (new in Orthanc 1.12.10)")
         .SetAnswerField(READ_ONLY, RestApiCallDocumentation::Type_Boolean,
                         "Whether Orthanc is running in read only mode (new in Orthanc 1.12.5)")
+        .SetAnswerField(PATIENT_LEVEL_ENABLED, RestApiCallDocumentation::Type_Boolean,
+                        "Whether Patient level routes and sanity checks are enabled (new in Orthanc 1.12.11)")
         .SetHttpGetSample("https://orthanc.uclouvain.be/demo/system", true);
       return;
     }
@@ -186,6 +189,7 @@
     result[STORAGE_AREA_PLUGIN] = Json::nullValue;
     result[DATABASE_BACKEND_PLUGIN] = Json::nullValue;
     result[READ_ONLY] = context.IsReadOnly();
+    result[PATIENT_LEVEL_ENABLED] = context.IsPatientLevelEnabled();
 
 #if ORTHANC_ENABLE_PLUGINS == 1
     result[PLUGINS_ENABLED] = true;
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Mar 16 10:47:50 2026 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Mon Mar 16 12:55:36 2026 +0100
@@ -400,6 +400,7 @@
     ingestTranscodingOfCompressed_(true),
     preferredTransferSyntax_(DicomTransferSyntax_LittleEndianExplicit),
     readOnly_(readOnly),
+    patientLevelEnabled_(true),
     deidentifyLogs_(false),
     serverStartTimeUtc_(boost::posix_time::second_clock::universal_time())
   {
@@ -624,6 +625,17 @@
   }
 
 
+  void ServerContext::SetPatientLevelEnabled(bool enabled)
+  {
+    if (enabled)
+      LOG(WARNING) << "Patient level is enabled";
+    else
+      LOG(WARNING) << "Patient level  is disabled";
+
+    patientLevelEnabled_ = enabled;
+  }
+
+
   void ServerContext::RemoveFile(const std::string& fileUuid,
                                  FileContentType type,
                                  const std::string& customData)
--- a/OrthancServer/Sources/ServerContext.h	Mon Mar 16 10:47:50 2026 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Mon Mar 16 12:55:36 2026 +0100
@@ -271,7 +271,8 @@
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
     std::list<std::string>         acceptedSopClasses_;  // ordered; the most 120 common ones first
     bool readOnly_;
-
+    bool patientLevelEnabled_;
+    
     StoreResult StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
                                       StoreInstanceMode mode,
@@ -342,6 +343,13 @@
       return storageCache_.SetMaximumSize(size);
     }
 
+    void SetPatientLevelEnabled(bool enabled);
+
+    bool IsPatientLevelEnabled() const
+    {
+      return patientLevelEnabled_;
+    }
+
     void SetCompressionEnabled(bool enabled);
 
     bool IsCompressionEnabled() const
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Mon Mar 16 10:47:50 2026 +0100
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Mon Mar 16 12:55:36 2026 +0100
@@ -727,7 +727,7 @@
       }
     }
 
-    if (modificationLevel == ResourceType_Study && replacePatientMainDicomTags)
+    if (GetContext().IsPatientLevelEnabled() && modificationLevel == ResourceType_Study && replacePatientMainDicomTags)
     {
       for (std::set<std::string>::const_iterator studyId = parentResources_.begin(); studyId != parentResources_.end(); ++studyId)
       {
--- a/OrthancServer/Sources/main.cpp	Mon Mar 16 10:47:50 2026 +0100
+++ b/OrthancServer/Sources/main.cpp	Mon Mar 16 12:55:36 2026 +0100
@@ -1679,6 +1679,8 @@
   {
     OrthancConfiguration::ReaderLock lock;
 
+    context.SetPatientLevelEnabled(lock.GetConfiguration().GetBooleanParameter("PatientLevelEnabled", true));
+
     if (context.IsReadOnly())
     {
       LOG(WARNING) << "READ-ONLY SYSTEM: ignoring these configurations: StorageCompression, StoreMD5ForAttachments, OverwriteInstances, MaximumPatientCount, MaximumStorageSize, MaximumStorageMode, SaveJobs"; 
@@ -1691,9 +1693,17 @@
       // New option in Orthanc 1.4.2
       context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
 
+      unsigned int maximumPatientCount = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0);
+      unsigned int maximumStorageSize = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
+
+      if (!context.IsPatientLevelEnabled() && (maximumPatientCount != 0 || maximumStorageSize != 0))
+      {
+        LOG(ERROR) << "Can not set MaximumPatientCount or MaximumStorageSize when PatientLevelEnabled is set to false since these options require patient recycling.";
+      }
+
       try
       {
-        context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
+        context.GetIndex().SetMaximumPatientCount(maximumPatientCount);
       }
       catch (...)
       {
@@ -1702,7 +1712,7 @@
 
       try
       {
-        uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
+        uint64_t size = maximumStorageSize;
         context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
       }
       catch (...)