changeset 4980:8b95fc86b8d9

merged more-tags -> default
author Alain Mazy <am@osimis.io>
date Mon, 25 Apr 2022 15:50:57 +0200
parents 5e7404f23fa8 (current diff) f316413027fd (diff)
children d0c34145320c
files
diffstat 46 files changed, 2967 insertions(+), 975 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Apr 13 10:51:22 2022 +0200
+++ b/NEWS	Mon Apr 25 15:50:57 2022 +0200
@@ -4,8 +4,43 @@
 General
 -------
 
+* New configuration "ExtraMainDicomTags" to store more tags in the Index DB
+  to speed up, e.g, building C-Find, dicom-web or tools/find answers
+* New sample plugin: "Housekeeper" that will re-construct the DB/Storage
+  when it detects there is room for improvements, e.g:
+  - if files were stored with a version of Orthanc prior to 1.9.1,
+    the storage might still contain dicom-as-json files that are not needed
+    anymore -> it will remove them
+  - if "ExtraMainDicomTags" has changed.
+  - if "StorageCompression" has chagned.
+* New configuration "Warnings" to enable/disable individual warnings that can
+  be identified by a W0XX prefix in the logs.
+  These warnings have been added:
+  - W001_TagsBeingReadFromStorage
+  - W002_InconsistentDicomTagsInDb
+* C-Find and QIDO-RS can now return the InstanceAvailability tag.  Value is 
+  always "ONLINE"
 * Improved decoding of US Images with Implicit VR.
-
+* Speed-up handling of DicomModalitiesInStudy in C-Find and tools/find queries.
+
+REST API
+--------
+
+* API version upgraded to 17
+* new options in tools/find:
+  - "RequestedTags" (to use together with "Expand": true) contains a list of tags 
+    that you'll receive in the "RequestedTags" field in the answers.  These tags
+    may be tags from the MainDicomTags in DB, from the DICOM file or 'computed'
+    like ModalitiesInStudy.  Check the new configuration "ExtraMainDicomTags" and
+    "Warnings" to optimize your queries.
+* new query argument "requestedTags" in all API routes listing resources:
+  - /patients, /patients/../studies, /patients/../series, /patients/../instances
+  - /studies, /studies/../series, /studies/../instances
+  - /series, /series/../instances
+  - /instances
+* new field "MainDicomTags" in the /system route response to list the tags that
+  are saved in DB
+* new field "StorageCompression" reported in the /system route response
 
 
 Version 1.10.1 (2022-03-23)
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Mon Apr 25 15:50:57 2022 +0200
@@ -38,7 +38,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "16")
+set(ORTHANC_API_VERSION "17")
 
 
 #####################################################################
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Mon Apr 25 15:50:57 2022 +0200
@@ -245,6 +245,11 @@
     "Name": "Revision",
     "Description": "A bad revision number was provided, which might indicate conflict between multiple writers"
   }, 
+  {
+    "Code": 44,
+    "Name": "MainDicomTagsMultiplyDefined",
+    "Description": "A main DICOM Tag has been defined multiple times for the same resource level"
+  }, 
 
 
 
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -69,6 +69,16 @@
     }
   }
 
+  void DicomArray::GetTags(std::set<DicomTag>& tags) const
+  {
+    tags.clear();
+
+    for (size_t i = 0; i < elements_.size(); i++)
+    {
+      tags.insert(elements_[i]->GetTag());
+    }
+   
+  }
 
   void DicomArray::Print(FILE* fp) const
   {
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.h	Mon Apr 25 15:50:57 2022 +0200
@@ -46,6 +46,8 @@
 
     const DicomElement& GetElement(size_t i) const;
 
+    void GetTags(std::set<DicomTag>& tags) const;
+
     void Print(FILE* fp) const;  // For debugging only
   };
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -26,6 +26,7 @@
 
 #include <stdio.h>
 #include <memory>
+#include <boost/algorithm/string/join.hpp>
 
 #include "../Compatibility.h"
 #include "../Endianness.h"
@@ -44,26 +45,38 @@
       const DicomTag tag_;
       const char*    name_;
     };
+    typedef std::vector<MainDicomTag> MainDicomTags;
+
   }
 
-  static const MainDicomTag PATIENT_MAIN_DICOM_TAGS[] =
+
+  // WARNING: the DEFAULT list of main dicom tags below are the list as they 
+  // were in Orthanc 1.10 before we introduced the dynamic main dicom tags.
+  // This list has not changed since Orthanc 1.4.2 and had a single change since
+  // Orthanc 0.9.5.
+  // These lists have a specific signature.  When a resource does not have
+  // the metadata "MainDicomTagsSignature", we'll assume that they were stored
+  // with an Orthanc prior to 1.11.  It is therefore very important that you never
+  // change these lists !
+
+  static const MainDicomTag DEFAULT_PATIENT_MAIN_DICOM_TAGS[] =
   {
     // { DicomTag(0x0010, 0x1010), "PatientAge" },
     // { DicomTag(0x0010, 0x1040), "PatientAddress" },
-    { DicomTag(0x0010, 0x0010), "PatientName" },
-    { DicomTag(0x0010, 0x0030), "PatientBirthDate" },
-    { DicomTag(0x0010, 0x0040), "PatientSex" },
-    { DicomTag(0x0010, 0x1000), "OtherPatientIDs" },
+    { DICOM_TAG_PATIENT_NAME, "PatientName" },
+    { DICOM_TAG_PATIENT_BIRTH_DATE, "PatientBirthDate" },
+    { DICOM_TAG_PATIENT_SEX, "PatientSex" },
+    { DICOM_TAG_OTHER_PATIENT_IDS, "OtherPatientIDs" },
     { DICOM_TAG_PATIENT_ID, "PatientID" }
   };
-    
-  static const MainDicomTag STUDY_MAIN_DICOM_TAGS[] =
+  
+  static const MainDicomTag DEFAULT_STUDY_MAIN_DICOM_TAGS[] =
   {
     // { DicomTag(0x0010, 0x1020), "PatientSize" },
     // { DicomTag(0x0010, 0x1030), "PatientWeight" },
     { DICOM_TAG_STUDY_DATE, "StudyDate" },
-    { DicomTag(0x0008, 0x0030), "StudyTime" },
-    { DicomTag(0x0020, 0x0010), "StudyID" },
+    { DICOM_TAG_STUDY_TIME, "StudyTime" },
+    { DICOM_TAG_STUDY_ID, "StudyID" },
     { DICOM_TAG_STUDY_DESCRIPTION, "StudyDescription" },
     { DICOM_TAG_ACCESSION_NUMBER, "AccessionNumber" },
     { DICOM_TAG_STUDY_INSTANCE_UID, "StudyInstanceUID" },
@@ -74,20 +87,20 @@
     { DICOM_TAG_REQUESTING_PHYSICIAN, "RequestingPhysician" },
     { DICOM_TAG_REFERRING_PHYSICIAN_NAME, "ReferringPhysicianName" }
   };
-    
-  static const MainDicomTag SERIES_MAIN_DICOM_TAGS[] =
+
+  static const MainDicomTag DEFAULT_SERIES_MAIN_DICOM_TAGS[] =
   {
     // { DicomTag(0x0010, 0x1080), "MilitaryRank" },
-    { DicomTag(0x0008, 0x0021), "SeriesDate" },
-    { DicomTag(0x0008, 0x0031), "SeriesTime" },
+    { DICOM_TAG_SERIES_DATE, "SeriesDate" },
+    { DICOM_TAG_SERIES_TIME, "SeriesTime" },
     { DICOM_TAG_MODALITY, "Modality" },
-    { DicomTag(0x0008, 0x0070), "Manufacturer" },
-    { DicomTag(0x0008, 0x1010), "StationName" },
+    { DICOM_TAG_MANUFACTURER, "Manufacturer" },
+    { DICOM_TAG_STATION_NAME, "StationName" },
     { DICOM_TAG_SERIES_DESCRIPTION, "SeriesDescription" },
-    { DicomTag(0x0018, 0x0015), "BodyPartExamined" },
-    { DicomTag(0x0018, 0x0024), "SequenceName" },
-    { DicomTag(0x0018, 0x1030), "ProtocolName" },
-    { DicomTag(0x0020, 0x0011), "SeriesNumber" },
+    { DICOM_TAG_BODY_PART_EXAMINED, "BodyPartExamined" },
+    { DICOM_TAG_SEQUENCE_NAME, "SequenceName" },
+    { DICOM_TAG_PROTOCOL_NAME, "ProtocolName" },
+    { DICOM_TAG_SERIES_NUMBER, "SeriesNumber" },
     { DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES, "CardiacNumberOfImages" },
     { DICOM_TAG_IMAGES_IN_ACQUISITION, "ImagesInAcquisition" },
     { DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS, "NumberOfTemporalPositions" },
@@ -103,12 +116,12 @@
     { DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION, "AcquisitionDeviceProcessingDescription" },
     { DICOM_TAG_CONTRAST_BOLUS_AGENT, "ContrastBolusAgent" }
   };
-    
-  static const MainDicomTag INSTANCE_MAIN_DICOM_TAGS[] =
+
+  static const MainDicomTag DEFAULT_INSTANCE_MAIN_DICOM_TAGS[] =
   {
-    { DicomTag(0x0008, 0x0012), "InstanceCreationDate" },
-    { DicomTag(0x0008, 0x0013), "InstanceCreationTime" },
-    { DicomTag(0x0020, 0x0012), "AcquisitionNumber" },
+    { DICOM_TAG_INSTANCE_CREATION_DATE, "InstanceCreationDate" },
+    { DICOM_TAG_INSTANCE_CREATION_TIME, "InstanceCreationTime" },
+    { DICOM_TAG_ACQUISITION_NUMBER, "AcquisitionNumber" },
     { DICOM_TAG_IMAGE_INDEX, "ImageIndex" },
     { DICOM_TAG_INSTANCE_NUMBER, "InstanceNumber" },
     { DICOM_TAG_NUMBER_OF_FRAMES, "NumberOfFrames" },
@@ -130,57 +143,6 @@
   };
 
 
-  static void LoadMainDicomTags(const MainDicomTag*& tags,
-                                size_t& size,
-                                ResourceType level)
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        tags = PATIENT_MAIN_DICOM_TAGS;
-        size = sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
-        break;
-
-      case ResourceType_Study:
-        tags = STUDY_MAIN_DICOM_TAGS;
-        size = sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
-        break;
-
-      case ResourceType_Series:
-        tags = SERIES_MAIN_DICOM_TAGS;
-        size = sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
-        break;
-
-      case ResourceType_Instance:
-        tags = INSTANCE_MAIN_DICOM_TAGS;
-        size = sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  static void LoadMainDicomTags(std::map<DicomTag, std::string>& target,
-                                ResourceType level)
-  {
-    const MainDicomTag* tags = NULL;
-    size_t size;
-    LoadMainDicomTags(tags, size, level);
-
-    assert(tags != NULL &&
-           size != 0);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      assert(target.find(tags[i].tag_) == target.end());
-      
-      target[tags[i].tag_] = tags[i].name_;
-    }
-  }
-
-
   namespace
   {
     class DicomTag2 : public DicomTag
@@ -199,23 +161,170 @@
   }
 
 
-  static void LoadMainDicomTags(std::map<std::string, DicomTag2>& target,
-                                ResourceType level)
+  class DicomMap::MainDicomTagsConfiguration
   {
-    const MainDicomTag* tags = NULL;
-    size_t size;
-    LoadMainDicomTags(tags, size, level);
+  private:
+    friend DicomMap;
+
+    // we keep many "copies" of the same data to guarantee quick access to organized data
+    // and avoid rebuilding it all the time.
+    std::map<ResourceType, std::map<DicomTag, std::string> > mainDicomTagsByTag_;
+    std::map<ResourceType, std::map<std::string, DicomTag2> > mainDicomTagsByName_;
+    std::map<ResourceType, std::set<DicomTag> > mainDicomTagsByLevel_;
+    std::set<DicomTag> allMainDicomTags_;
+
+    std::map<ResourceType, std::string> signatures_;
+    std::map<ResourceType, std::string> defaultSignatures_;
+
+    MainDicomTagsConfiguration()
+    {
+      ResetDefaultMainDicomTags();
+    }
+
+    void ResetDefaultMainDicomTags()
+    {
+      mainDicomTagsByTag_.clear();
+      mainDicomTagsByName_.clear();
+      mainDicomTagsByLevel_.clear();
+      allMainDicomTags_.clear();
+
+      // by default, initialize with the previous static list (up to 1.10.0)
+      LoadDefaultMainDicomTags(ResourceType_Patient);
+      LoadDefaultMainDicomTags(ResourceType_Study);
+      LoadDefaultMainDicomTags(ResourceType_Series);
+      LoadDefaultMainDicomTags(ResourceType_Instance);
+
+      defaultSignatures_[ResourceType_Patient] = signatures_[ResourceType_Patient];
+      defaultSignatures_[ResourceType_Study] = signatures_[ResourceType_Study];
+      defaultSignatures_[ResourceType_Series] = signatures_[ResourceType_Series];
+      defaultSignatures_[ResourceType_Instance] = signatures_[ResourceType_Instance];
+    }
+
+    std::string ComputeSignature(const std::set<DicomTag>& tags)
+    {
+      // std::set are sorted by default (which is important for us !)
+      std::set<std::string> tagsIds;
+      for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); it++)
+      {
+        tagsIds.insert(it->Format());
+      }
+
+      std::string signatureText = boost::algorithm::join(tagsIds, ";");
+
+      return signatureText;
+    }
+
+    void LoadDefaultMainDicomTags(ResourceType level)
+    {
+      assert(mainDicomTagsByTag_.find(level) == mainDicomTagsByTag_.end());
+
+      const MainDicomTag* tags = NULL;
+      size_t size;
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          tags = DEFAULT_PATIENT_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
+          break;
+
+        case ResourceType_Study:
+          tags = DEFAULT_STUDY_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
+          break;
+
+        case ResourceType_Series:
+          tags = DEFAULT_SERIES_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
+          break;
+
+        case ResourceType_Instance:
+          tags = DEFAULT_INSTANCE_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
+          break;
 
-    assert(tags != NULL &&
-           size != 0);
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      assert(tags != NULL &&
+            size != 0);
+
+      for (size_t i = 0; i < size; i++)
+      {
+        AddMainDicomTag(tags[i].tag_, tags[i].name_, level);
+      }
 
-    for (size_t i = 0; i < size; i++)
+    }
+
+  public:
+    // Singleton pattern
+    static MainDicomTagsConfiguration& GetInstance()
+    {
+      static MainDicomTagsConfiguration parameters;
+      return parameters;
+    }
+
+    void AddMainDicomTag(const DicomTag& tag, const std::string& name, ResourceType level)
     {
-      assert(target.find(tags[i].name_) == target.end());
-      
-      target[tags[i].name_] = DicomTag2(tags[i].tag_);
+      if (mainDicomTagsByTag_[level].find(tag) != mainDicomTagsByTag_[level].end())
+      {
+        throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined");
+      }
+
+      if (mainDicomTagsByName_[level].find(name) != mainDicomTagsByName_[level].end())
+      {
+        throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, name + " is already defined");
+      }
+
+      mainDicomTagsByTag_[level][tag] = name;
+      mainDicomTagsByName_[level][name] = DicomTag2(tag);
+      mainDicomTagsByLevel_[level].insert(tag);
+      allMainDicomTags_.insert(tag);
+      signatures_[level] = ComputeSignature(GetMainDicomTagsByLevel(level));
+    }
+
+    const std::map<DicomTag, std::string>& GetMainDicomTags(ResourceType level) const
+    {
+      assert(mainDicomTagsByTag_.find(level) != mainDicomTagsByTag_.end());
+
+      return mainDicomTagsByTag_.at(level);
     }
-  }
+
+    const std::map<std::string, DicomTag2>& GetMainDicomTagsByName(ResourceType level) const
+    {
+      assert(mainDicomTagsByName_.find(level) != mainDicomTagsByName_.end());
+
+      return mainDicomTagsByName_.at(level);
+    }
+
+    const std::set<DicomTag>& GetMainDicomTagsByLevel(ResourceType level) const
+    {
+      assert(mainDicomTagsByLevel_.find(level) != mainDicomTagsByLevel_.end());
+
+      return mainDicomTagsByLevel_.at(level);
+    }
+
+    const std::set<DicomTag>& GetAllMainDicomTags() const
+    {
+      return allMainDicomTags_;
+    }
+
+    const std::string& GetMainDicomTagsSignature(ResourceType level)
+    {
+      assert(signatures_.find(level) != signatures_.end());
+
+      return signatures_[level];
+    }
+
+    const std::string& GetDefaultMainDicomTagsSignature(ResourceType level)
+    {
+      assert(defaultSignatures_.find(level) != defaultSignatures_.end());
+
+      return defaultSignatures_[level];
+    }
+
+  };
 
 
   void DicomMap::SetValueInternal(uint16_t group, 
@@ -294,16 +403,17 @@
   }
 
 
-  static void ExtractTags(DicomMap& result,
+  // MORE_TAGS: TODO: we can probably remove the std::string from MainDicomTags (not used here !!!)
+  static void ExtractTagsInternal(DicomMap& result,
                           const DicomMap::Content& source,
-                          const MainDicomTag* tags,
-                          size_t count)
+                          const std::map<DicomTag, std::string>& mainDicomTags)
   {
     result.Clear();
 
-    for (unsigned int i = 0; i < count; i++)
+    for (std::map<DicomTag, std::string>::const_iterator itmt = mainDicomTags.begin();
+         itmt != mainDicomTags.end(); itmt++)
     {
-      DicomMap::Content::const_iterator it = source.find(tags[i].tag_);
+      DicomMap::Content::const_iterator it = source.find(itmt->first);
       if (it != source.end())
       {
         result.SetValue(it->first, *it->second /* value will be cloned */);
@@ -311,25 +421,45 @@
     }
   }
 
+  void DicomMap::ExtractTags(DicomMap& result, const std::set<DicomTag>& tags) const
+  {
+    result.Clear();
+
+    for (std::set<DicomTag>::const_iterator itmt = tags.begin();
+         itmt != tags.end(); itmt++)
+    {
+      DicomMap::Content::const_iterator it = content_.find(*itmt);
+      if (it != content_.end())
+      {
+        result.SetValue(it->first, *it->second /* value will be cloned */);
+      }
+    }
+  }
+
+  void DicomMap::ExtractResourceInformation(DicomMap& result, ResourceType level) const
+  {
+    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(level);
+    ExtractTagsInternal(result, content_, mainDicomTags);
+  }
 
   void DicomMap::ExtractPatientInformation(DicomMap& result) const
   {
-    ExtractTags(result, content_, PATIENT_MAIN_DICOM_TAGS, sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    ExtractResourceInformation(result, ResourceType_Patient);
   }
 
   void DicomMap::ExtractStudyInformation(DicomMap& result) const
   {
-    ExtractTags(result, content_, STUDY_MAIN_DICOM_TAGS, sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    ExtractResourceInformation(result, ResourceType_Study);
   }
 
   void DicomMap::ExtractSeriesInformation(DicomMap& result) const
   {
-    ExtractTags(result, content_, SERIES_MAIN_DICOM_TAGS, sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    ExtractResourceInformation(result, ResourceType_Series);
   }
 
   void DicomMap::ExtractInstanceInformation(DicomMap& result) const
   {
-    ExtractTags(result, content_, INSTANCE_MAIN_DICOM_TAGS, sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    ExtractResourceInformation(result, ResourceType_Instance);
   }
 
 
@@ -415,25 +545,27 @@
 
 
   static void SetupFindTemplate(DicomMap& result,
-                                const MainDicomTag* tags,
-                                size_t count) 
+                                const std::map<DicomTag, std::string>& mainDicomTags)
   {
     result.Clear();
 
-    for (size_t i = 0; i < count; i++)
+    for (std::map<DicomTag, std::string>::const_iterator itmt = mainDicomTags.begin();
+         itmt != mainDicomTags.end(); itmt++)
     {
-      result.SetValue(tags[i].tag_, "", false);
+      result.SetValue(itmt->first, "", false);
     }
   }
 
   void DicomMap::SetupFindPatientTemplate(DicomMap& result)
   {
-    SetupFindTemplate(result, PATIENT_MAIN_DICOM_TAGS, sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Patient);
+    SetupFindTemplate(result, mainDicomTags);
   }
 
   void DicomMap::SetupFindStudyTemplate(DicomMap& result)
   {
-    SetupFindTemplate(result, STUDY_MAIN_DICOM_TAGS, sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Study);
+    SetupFindTemplate(result, mainDicomTags);
     result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
     result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
 
@@ -446,7 +578,8 @@
 
   void DicomMap::SetupFindSeriesTemplate(DicomMap& result)
   {
-    SetupFindTemplate(result, SERIES_MAIN_DICOM_TAGS, sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Series);
+    SetupFindTemplate(result, mainDicomTags);
     result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
     result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
     result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
@@ -468,7 +601,8 @@
 
   void DicomMap::SetupFindInstanceTemplate(DicomMap& result)
   {
-    SetupFindTemplate(result, INSTANCE_MAIN_DICOM_TAGS, sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Instance);
+    SetupFindTemplate(result, mainDicomTags);
     result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false);
     result.SetValue(DICOM_TAG_PATIENT_ID, "", false);
     result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false);
@@ -488,19 +622,8 @@
 
   bool DicomMap::IsMainDicomTag(const DicomTag& tag, ResourceType level)
   {
-    const MainDicomTag *tags = NULL;
-    size_t size;
-    LoadMainDicomTags(tags, size, level);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      if (tags[i].tag_ == tag)
-      {
-        return true;
-      }
-    }
-
-    return false;
+    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(level);
+    return mainDicomTags.find(tag) != mainDicomTags.end();
   }
 
   bool DicomMap::IsMainDicomTag(const DicomTag& tag)
@@ -511,36 +634,124 @@
             IsMainDicomTag(tag, ResourceType_Instance));
   }
 
+  static bool IsGenericComputedTag(const DicomTag& tag)
+  {
+    return tag == DICOM_TAG_RETRIEVE_URL ||
+      tag == DICOM_TAG_RETRIEVE_AE_TITLE;
+  }
 
-  void DicomMap::GetMainDicomTagsInternal(std::set<DicomTag>& result, ResourceType level)
+  bool DicomMap::IsComputedTag(const DicomTag& tag)
+  {
+    return (IsComputedTag(tag, ResourceType_Patient) ||
+            IsComputedTag(tag, ResourceType_Study) ||
+            IsComputedTag(tag, ResourceType_Series) ||
+            IsComputedTag(tag, ResourceType_Instance) ||
+            IsGenericComputedTag(tag));
+  }
+
+  bool DicomMap::IsComputedTag(const DicomTag& tag, ResourceType level)
   {
-    const MainDicomTag *tags = NULL;
-    size_t size;
-    LoadMainDicomTags(tags, size, level);
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return (
+          tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES ||
+          tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES ||
+          tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES
+        );
+      case ResourceType_Study:
+        return (
+          tag == DICOM_TAG_MODALITIES_IN_STUDY ||
+          tag == DICOM_TAG_SOP_CLASSES_IN_STUDY ||
+          tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES ||
+          tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES
+        );
+      case ResourceType_Series:
+        return (
+          tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES
+        );
+      case ResourceType_Instance:
+        return (
+          tag == DICOM_TAG_INSTANCE_AVAILABILITY
+        );
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
 
-    for (size_t i = 0; i < size; i++)
+  bool DicomMap::HasOnlyComputedTags(const std::set<DicomTag>& tags)
+  {
+    if (tags.size() == 0)
+    {
+      return false;
+    }
+
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
     {
-      result.insert(tags[i].tag_);
+      if (!IsComputedTag(*it))
+      {
+        return false;
+      }
     }
+    return true;
+  }
+
+  bool DicomMap::HasComputedTags(const std::set<DicomTag>& tags)
+  {
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (IsComputedTag(*it))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  bool DicomMap::HasComputedTags(const std::set<DicomTag>& tags, ResourceType level)
+  {
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (IsComputedTag(*it, level))
+      {
+        return true;
+      }
+    }
+    return false;
   }
 
 
-  void DicomMap::GetMainDicomTags(std::set<DicomTag>& result, ResourceType level)
+  const std::set<DicomTag>& DicomMap::GetMainDicomTags(ResourceType level)
+  {
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(level);
+  }
+
+  const std::set<DicomTag>& DicomMap::GetAllMainDicomTags()
   {
-    result.clear();
-    GetMainDicomTagsInternal(result, level);
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetAllMainDicomTags();
+  }
+
+  void DicomMap::AddMainDicomTag(const DicomTag& tag, const std::string& name, ResourceType level)
+  {
+    DicomMap::MainDicomTagsConfiguration::GetInstance().AddMainDicomTag(tag, name, level);
   }
 
-
-  void DicomMap::GetMainDicomTags(std::set<DicomTag>& result)
+  void DicomMap::ResetDefaultMainDicomTags()
   {
-    result.clear();
-    GetMainDicomTagsInternal(result, ResourceType_Patient);
-    GetMainDicomTagsInternal(result, ResourceType_Study);
-    GetMainDicomTagsInternal(result, ResourceType_Series);
-    GetMainDicomTagsInternal(result, ResourceType_Instance);
+    DicomMap::MainDicomTagsConfiguration::GetInstance().ResetDefaultMainDicomTags();
   }
 
+  const std::string& DicomMap::GetMainDicomTagsSignature(ResourceType level)
+  {
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsSignature(level);
+  }
+
+  const std::string& DicomMap::GetDefaultMainDicomTagsSignature(ResourceType level)
+  {
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignature(level);
+  }
 
   void DicomMap::GetTags(std::set<DicomTag>& tags) const
   {
@@ -1201,21 +1412,18 @@
   void DicomMap::MergeMainDicomTags(const DicomMap& other,
                                     ResourceType level)
   {
-    const MainDicomTag* tags = NULL;
-    size_t size = 0;
+    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(level);
 
-    LoadMainDicomTags(tags, size, level);
-    assert(tags != NULL && size > 0);
-
-    for (size_t i = 0; i < size; i++)
+    for (std::map<DicomTag, std::string>::const_iterator itmt = mainDicomTags.begin();
+         itmt != mainDicomTags.end(); itmt++)
     {
-      Content::const_iterator found = other.content_.find(tags[i].tag_);
+      Content::const_iterator found = other.content_.find(itmt->first);
 
       if (found != other.content_.end() &&
-          content_.find(tags[i].tag_) == content_.end())
+          content_.find(itmt->first) == content_.end())
       {
         assert(found->second != NULL);
-        content_[tags[i].tag_] = found->second->Clone();
+        content_[itmt->first] = found->second->Clone();
       }
     }
   }
@@ -1233,14 +1441,11 @@
 
   bool DicomMap::HasOnlyMainDicomTags() const
   {
-    // TODO - Speed up possible by making this std::set a global variable
-
-    std::set<DicomTag> mainDicomTags;
-    GetMainDicomTags(mainDicomTags);
+    const std::set<DicomTag>& allMainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetAllMainDicomTags();
 
     for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
     {
-      if (mainDicomTags.find(it->first) == mainDicomTags.end())
+      if (allMainDicomTags.find(it->first) == allMainDicomTags.end())
       {
         return false;
       }
@@ -1475,8 +1680,7 @@
   void DicomMap::DumpMainDicomTags(Json::Value& target,
                                    ResourceType level) const
   {
-    std::map<DicomTag, std::string> mainTags;   // TODO - Create a singleton to hold this map
-    LoadMainDicomTags(mainTags, level);
+    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(level);
     
     target = Json::objectValue;
 
@@ -1487,9 +1691,9 @@
       if (!it->second->IsBinary() &&
           !it->second->IsNull())
       {
-        std::map<DicomTag, std::string>::const_iterator found = mainTags.find(it->first);
+        std::map<DicomTag, std::string>::const_iterator found = mainDicomTags.find(it->first);
 
-        if (found != mainTags.end())
+        if (found != mainDicomTags.end())
         {
           target[found->second] = it->second->GetContent();
         }
@@ -1506,8 +1710,7 @@
       throw OrthancException(ErrorCode_BadFileFormat);
     }
     
-    std::map<std::string, DicomTag2> mainTags;   // TODO - Create a singleton to hold this map
-    LoadMainDicomTags(mainTags, level);
+    const std::map<std::string, DicomTag2>& mainTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByName(level);
     
     Json::Value::Members members = source.getMemberNames();
     for (size_t i = 0; i < members.size(); i++)
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Mon Apr 25 15:50:57 2022 +0200
@@ -31,18 +31,27 @@
 #include <map>
 #include <json/value.h>
 
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+#  include <gtest/gtest_prod.h>
+#endif
+
 namespace Orthanc
 {
   class ORTHANC_PUBLIC DicomMap : public boost::noncopyable
   {
   public:
     typedef std::map<DicomTag, DicomValue*>  Content;
-    
+
   private:
+    class MainDicomTagsConfiguration;
     friend class DicomArray;
     friend class FromDcmtkBridge;
     friend class ParsedDicomFile;
 
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+    friend class DicomMapMainTagsTests;
+#endif
+
     Content content_;
 
     // Warning: This takes the ownership of "value"
@@ -50,8 +59,8 @@
                           uint16_t element, 
                           DicomValue* value);
 
-    static void GetMainDicomTagsInternal(std::set<DicomTag>& result,
-                                         ResourceType level);
+    // used for unit tests only
+    static void ResetDefaultMainDicomTags();
 
   public:
     ~DicomMap();
@@ -109,6 +118,10 @@
 
     void ExtractInstanceInformation(DicomMap& result) const;
 
+    void ExtractResourceInformation(DicomMap& result, ResourceType level) const;
+
+    void ExtractTags(DicomMap& result, const std::set<DicomTag>& tags) const;
+
     static void SetupFindPatientTemplate(DicomMap& result);
 
     static void SetupFindStudyTemplate(DicomMap& result);
@@ -124,9 +137,28 @@
 
     static bool IsMainDicomTag(const DicomTag& tag);
 
-    static void GetMainDicomTags(std::set<DicomTag>& result, ResourceType level);
+    static bool IsComputedTag(const DicomTag& tag, ResourceType level);
+
+    static bool IsComputedTag(const DicomTag& tag);
+
+    static bool HasOnlyComputedTags(const std::set<DicomTag>& tags);
+
+    static bool HasComputedTags(const std::set<DicomTag>& tags, ResourceType level);
+
+    static bool HasComputedTags(const std::set<DicomTag>& tags);
 
-    static void GetMainDicomTags(std::set<DicomTag>& result);
+    static const std::set<DicomTag>& GetMainDicomTags(ResourceType level);
+
+    // returns a string uniquely identifying the list of main dicom tags for a level
+    static const std::string& GetMainDicomTagsSignature(ResourceType level);
+
+    static const std::string& GetDefaultMainDicomTagsSignature(ResourceType level);
+
+    static const std::set<DicomTag>& GetAllMainDicomTags();
+
+    // adds a main dicom tag to the definition of main dicom tags for each level.
+    // this should be done once at startup before you use MainDicomTags methods
+    static void AddMainDicomTag(const DicomTag& tag, const std::string& name, ResourceType level);
 
     void GetTags(std::set<DicomTag>& tags) const;
 
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Mon Apr 25 15:50:57 2022 +0200
@@ -117,6 +117,7 @@
   static const DicomTag DICOM_TAG_QUERY_RETRIEVE_LEVEL(0x0008, 0x0052);
   static const DicomTag DICOM_TAG_MODALITIES_IN_STUDY(0x0008, 0x0061);
   static const DicomTag DICOM_TAG_RETRIEVE_AE_TITLE(0x0008, 0x0054);
+  static const DicomTag DICOM_TAG_INSTANCE_AVAILABILITY(0x0008, 0x0056);
 
   // Tags for images
   static const DicomTag DICOM_TAG_COLUMNS(0x0028, 0x0011);
@@ -166,8 +167,11 @@
   static const DicomTag DICOM_TAG_PATIENT_SEX(0x0010, 0x0040);
   static const DicomTag DICOM_TAG_LATERALITY(0x0020, 0x0060);
   static const DicomTag DICOM_TAG_BODY_PART_EXAMINED(0x0018, 0x0015);
+  static const DicomTag DICOM_TAG_SEQUENCE_NAME(0x0018, 0x0024);
+  static const DicomTag DICOM_TAG_PROTOCOL_NAME(0x0018, 0x1030);
   static const DicomTag DICOM_TAG_VIEW_POSITION(0x0018, 0x5101);
   static const DicomTag DICOM_TAG_MANUFACTURER(0x0008, 0x0070);
+  static const DicomTag DICOM_TAG_STATION_NAME(0x0008, 0x1010);
   static const DicomTag DICOM_TAG_PATIENT_ORIENTATION(0x0020, 0x0020);
   static const DicomTag DICOM_TAG_PATIENT_COMMENTS(0x0010, 0x4000);
   static const DicomTag DICOM_TAG_PATIENT_SPECIES_DESCRIPTION(0x0010, 0x2201);
@@ -176,6 +180,7 @@
   static const DicomTag DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUP_SEQUENCE(0x5200, 0x9230);
   static const DicomTag DICOM_TAG_PIXEL_VALUE_TRANSFORMATION_SEQUENCE(0x0028, 0x9145);
   static const DicomTag DICOM_TAG_FRAME_VOI_LUT_SEQUENCE(0x0028, 0x9132);
+  static const DicomTag DICOM_TAG_ACQUISITION_NUMBER(0x0020, 0x0012);
 
   // Tags used within the Stone of Orthanc
   static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009);
@@ -225,4 +230,8 @@
   static const DicomTag DICOM_TAG_OFFSET_OF_REFERENCED_LOWER_LEVEL_DIRECTORY_ENTITY(0x0004, 0x1420);
   static const DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE(0x0004, 0x1511);
   static const DicomTag DICOM_TAG_REFERENCED_FILE_ID(0x0004, 0x1500);
+
+  // Tags for DicomWeb
+  static const Orthanc::DicomTag DICOM_TAG_RETRIEVE_URL(0x0008, 0x1190);
+
 }
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -1296,7 +1296,7 @@
     else
     {
       CLOG(INFO, DICOM) << "Unknown DICOM tag: \"" << name << "\"";
-      throw OrthancException(ErrorCode_UnknownDicomTag);
+      throw OrthancException(ErrorCode_UnknownDicomTag, name, false);
     }
 #endif
   }
@@ -1311,6 +1311,65 @@
     return fields.HasTag(ParseTag(tagName));
   }
 
+  void FromDcmtkBridge::FormatListOfTags(std::string& output, const std::set<DicomTag>& tags)
+  {
+    std::set<std::string> values;
+    for (std::set<DicomTag>::const_iterator it = tags.begin();
+         it != tags.end(); it++)
+    {
+      values.insert(it->Format());
+    }
+
+    Toolbox::JoinStrings(output, values, ";");
+  }
+
+  void FromDcmtkBridge::FormatListOfTags(Json::Value& output, const std::set<DicomTag>& tags)
+  {
+    output = Json::arrayValue;
+    for (std::set<DicomTag>::const_iterator it = tags.begin();
+         it != tags.end(); it++)
+    {
+      output.append(it->Format());
+    }
+  }
+
+  // parses a list like "0010,0010;PatientBirthDate;0020,0020"
+  void FromDcmtkBridge::ParseListOfTags(std::set<DicomTag>& result, const std::string& source)
+  {
+    result.clear();
+
+    std::vector<std::string> tokens;
+    Toolbox::TokenizeString(tokens, source, ';');
+
+    for (std::vector<std::string>::const_iterator it = tokens.begin();
+         it != tokens.end(); it++)
+    {
+      if (it->size() > 0)
+      {
+        DicomTag tag = FromDcmtkBridge::ParseTag(*it);
+        result.insert(tag);
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::ParseListOfTags(std::set<DicomTag>& result, const Json::Value& source)
+  {
+    result.clear();
+
+    if (!source.isArray())
+    {
+      throw OrthancException(ErrorCode_BadRequest, "List of tags is not an array");
+    }
+
+    for (Json::ArrayIndex i = 0; i < source.size(); i++)
+    {
+      const std::string& value = source[i].asString();
+      DicomTag tag = FromDcmtkBridge::ParseTag(value);
+      result.insert(tag);
+    }
+  }
+
   const DicomValue &FromDcmtkBridge::GetValue(const DicomMap &fields,
                                               const std::string &tagName)
   {
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Mon Apr 25 15:50:57 2022 +0200
@@ -175,6 +175,15 @@
 
     static DicomTag ParseTag(const std::string& name);
 
+    // parses a list like "0010,0010;PatientBirthDate;0020,0020"
+    static void ParseListOfTags(std::set<DicomTag>& result, const std::string& source);
+
+    static void ParseListOfTags(std::set<DicomTag>& result, const Json::Value& source);
+
+    static void FormatListOfTags(std::string& output, const std::set<DicomTag>& tags);
+
+    static void FormatListOfTags(Json::Value& output, const std::set<DicomTag>& tags);
+
     static bool HasTag(const DicomMap& fields,
                        const std::string& tagName);
 
--- a/OrthancFramework/Sources/Enumerations.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -55,7 +55,7 @@
   static const char* const MIME_ICO = "image/x-icon";
 
   // This function is autogenerated by the script
-  // "Resources/GenerateErrorCodes.py"
+  // "Resources/CodeGeneration/GenerateErrorCodes.py"
   const char* EnumerationToString(ErrorCode error)
   {
     switch (error)
@@ -195,6 +195,9 @@
       case ErrorCode_Revision:
         return "A bad revision number was provided, which might indicate conflict between multiple writers";
 
+      case ErrorCode_MainDicomTagsMultiplyDefined:
+        return "A main DICOM Tag has been defined multiple times for the same resource level";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -2159,7 +2162,7 @@
 
 
   // This function is autogenerated by the script
-  // "Resources/GenerateErrorCodes.py"
+  // "Resources/CodeGeneration/GenerateErrorCodes.py"
   HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error)
   {
     switch (error)
--- a/OrthancFramework/Sources/Enumerations.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Mon Apr 25 15:50:57 2022 +0200
@@ -91,7 +91,7 @@
   };
 
   // This enumeration is autogenerated by the script
-  // "Resources/GenerateErrorCodes.py"
+  // "Resources/CodeGeneration/GenerateErrorCodes.py"
   enum ErrorCode
   {
     ErrorCode_InternalError = -1    /*!< Internal error */,
@@ -139,6 +139,7 @@
     ErrorCode_BadRange = 41    /*!< Incorrect range request */,
     ErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
     ErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
+    ErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
--- a/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Mon Apr 25 15:50:57 2022 +0200
@@ -52,7 +52,7 @@
 {
   namespace SQLite
   {
-    // Auto-generated by "Resources/GenerateErrorCodes.py"
+    // Auto-generated by "Resources/CodeGeneration/GenerateErrorCodes.py"
     enum ErrorCode
     {
       ErrorCode_ParameterOutOfRange,
@@ -83,7 +83,7 @@
       {
       }
 
-      // Auto-generated by "Resources/GenerateErrorCodes.py"
+      // Auto-generated by "Resources/CodeGeneration/GenerateErrorCodes.py"
       static const char* EnumerationToString(ErrorCode code)
       {
         switch (code)
--- a/OrthancFramework/Sources/Toolbox.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -56,6 +56,7 @@
 
 #include <boost/algorithm/string/case_conv.hpp>
 #include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/join.hpp>
 #include <boost/lexical_cast.hpp>
 #include <boost/regex.hpp>
 
@@ -1032,6 +1033,21 @@
   }
 
 
+  void Toolbox::JoinStrings(std::string& result,
+                            std::set<std::string>& source,
+                            const char* separator)
+  {
+    result = boost::algorithm::join(source, separator);
+  }
+
+  void JoinStrings(std::string& result,
+                   std::vector<std::string>& source,
+                   const char* separator)
+  {
+    result = boost::algorithm::join(source, separator);
+  }
+
+
 #if ORTHANC_ENABLE_PUGIXML == 1
   class ChunkedBufferWriter : public pugi::xml_writer
   {
--- a/OrthancFramework/Sources/Toolbox.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Mon Apr 25 15:50:57 2022 +0200
@@ -185,6 +185,64 @@
                                const std::string& source,
                                char separator);
 
+    static void JoinStrings(std::string& result,
+                            std::set<std::string>& source,
+                            const char* separator);
+
+    static void JoinStrings(std::string& result,
+                            std::vector<std::string>& source,
+                            const char* separator);
+
+    // returns true if all element of 'needles' are found in 'haystack'
+    template <typename T> static bool IsSetInSet(const std::set<T>& needles, const std::set<T>& haystack)
+    {
+      for (typename std::set<T>::const_iterator it = needles.begin();
+            it != needles.end(); it++)
+      {
+        if (haystack.count(*it) == 0)
+        {
+          return false;
+        }
+      }
+
+      return true;
+    }
+
+    // returns the set of elements from 'needles' that are not in 'haystack'
+    template <typename T> static size_t GetMissingsFromSet(std::set<T>& missings, const std::set<T>& needles, const std::set<T>& haystack)
+    {
+      missings.clear();
+
+      for (typename std::set<T>::const_iterator it = needles.begin();
+            it != needles.end(); it++)
+      {
+        if (haystack.count(*it) == 0)
+        {
+          missings.insert(*it);
+        }
+      }
+
+      return missings.size();
+    }
+
+    template <typename T> static void AppendSets(std::set<T>& target, const std::set<T>& toAppend)
+    {
+      for (typename std::set<T>::const_iterator it = toAppend.begin();
+            it != toAppend.end(); it++)
+      {
+        target.insert(*it);
+      }
+    }
+
+    template <typename T> static void RemoveSets(std::set<T>& target, const std::set<T>& toRemove)
+    {
+      for (typename std::set<T>::const_iterator it = toRemove.begin();
+            it != toRemove.end(); it++)
+      {
+        target.erase(*it);
+      }
+    }
+
 #if ORTHANC_ENABLE_PUGIXML == 1
     static void JsonToXml(std::string& target,
                           const Json::Value& source,
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -45,44 +45,130 @@
 
 using namespace Orthanc;
 
-TEST(DicomMap, MainTags)
+
+namespace Orthanc
 {
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient));
-  ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study));
+  // The namespace is necessary because of FRIEND_TEST
+  // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members
+
+  class DicomMapMainTagsTests : public ::testing::Test
+  {
+  public:
+    DicomMapMainTagsTests()
+    {
+    }
+
+    virtual void SetUp() ORTHANC_OVERRIDE
+    {
+      DicomMap::ResetDefaultMainDicomTags();
+    }
+
+    virtual void TearDown() ORTHANC_OVERRIDE
+    {
+      DicomMap::ResetDefaultMainDicomTags();
+    }
+  };
 
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID));
+  TEST_F(DicomMapMainTagsTests, MainTags)
+  {
+    ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID));
+    ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient));
+    ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study));
+
+    ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
+    ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER));
+    ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
+    ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID));
 
-  std::set<DicomTag> s;
-  DicomMap::GetMainDicomTags(s);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
+    {
+      const std::set<DicomTag>& s = DicomMap::GetAllMainDicomTags();
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
+    }
+
+    {
+      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Patient);
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
+      ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+    }
+
+    {
+      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Study);
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
+      ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+    }
 
-  DicomMap::GetMainDicomTags(s, ResourceType_Patient);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+    {
+      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Series);
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
+      ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+    }
+
+    {
+      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Instance);
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
+      ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+    }
+  }
 
-  DicomMap::GetMainDicomTags(s, ResourceType_Study);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+  TEST_F(DicomMapMainTagsTests, AddMainTags)
+  {
+    DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, "BitsAllocated", ResourceType_Instance);
+
+    {
+      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Instance);
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_BITS_ALLOCATED));
+      ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
+    }
+    {
+      const std::set<DicomTag>& s = DicomMap::GetMainDicomTags(ResourceType_Series);
+      ASSERT_TRUE(s.end() == s.find(DICOM_TAG_BITS_ALLOCATED));
+    }
+
+    ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_BITS_ALLOCATED));
+    ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance));
+
+    // adding the same tag should throw
+    ASSERT_THROW(DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, "BitsAllocated", ResourceType_Instance), OrthancException);
 
-  DicomMap::GetMainDicomTags(s, ResourceType_Series);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+    // adding another tag with same name should throw
+    ASSERT_THROW(DicomMap::AddMainDicomTag(DICOM_TAG_BITS_STORED, "BitsAllocated", ResourceType_Instance), OrthancException);
+  }
+
+  TEST_F(DicomMapMainTagsTests, Signatures)
+  {
+    std::string defaultPatientSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Patient);
+    std::string defaultStudySignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Study);
+    std::string defaultSeriesSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Series);
+    std::string defaultInstanceSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Instance);
+
+    ASSERT_NE(defaultInstanceSignature, defaultPatientSignature);
+    ASSERT_NE(defaultSeriesSignature, defaultStudySignature);
+    ASSERT_NE(defaultSeriesSignature, defaultPatientSignature);
 
-  DicomMap::GetMainDicomTags(s, ResourceType_Instance);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+    std::string patientSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Patient);
+    std::string studySignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Study);
+    std::string seriesSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Series);
+    std::string instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance);
+
+    // at start, default and current signature should be equal
+    ASSERT_EQ(defaultPatientSignature, patientSignature);
+    ASSERT_EQ(defaultStudySignature, studySignature);
+    ASSERT_EQ(defaultSeriesSignature, seriesSignature);
+    ASSERT_EQ(defaultInstanceSignature, instanceSignature);
+
+    DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, "BitsAllocated", ResourceType_Instance);
+    instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance);
+    
+    ASSERT_NE(defaultInstanceSignature, instanceSignature);
+  }
+
 }
 
-
 TEST(DicomMap, Tags)
 {
   std::set<DicomTag> s;
@@ -157,9 +243,9 @@
   // REFERENCE: DICOM PS3.3 2015c - Information Object Definitions
   // http://dicom.nema.org/medical/dicom/current/output/html/part03.html
 
-  std::set<DicomTag> moduleTags, main;
+  std::set<DicomTag> moduleTags;
+  const std::set<DicomTag>& main = DicomMap::GetMainDicomTags(level);
   DicomTag::AddTagsForModule(moduleTags, module);
-  DicomMap::GetMainDicomTags(main, level);
   
   // The main dicom tags are a subset of the module
   for (std::set<DicomTag>::const_iterator it = main.begin(); it != main.end(); ++it)
@@ -470,6 +556,55 @@
 }
 
 
+TEST(DicomMap, ComputedTags)
+{
+  {
+    std::set<DicomTag> tags;
+
+    ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Study));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient));
+  }
+
+  {
+    std::set<DicomTag> tags;
+    tags.insert(DICOM_TAG_ACCESSION_NUMBER);
+
+    ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Study));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient));
+  }
+
+  {
+    std::set<DicomTag> tags;
+    tags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+    tags.insert(DICOM_TAG_RETRIEVE_URL);
+
+    ASSERT_TRUE(DicomMap::HasOnlyComputedTags(tags));
+    ASSERT_TRUE(DicomMap::HasComputedTags(tags, ResourceType_Study));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance));
+  }
+
+  {
+    std::set<DicomTag> tags;
+    tags.insert(DICOM_TAG_ACCESSION_NUMBER);
+    tags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+
+    ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags));
+    ASSERT_TRUE(DicomMap::HasComputedTags(tags, ResourceType_Study));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance));
+  }
+
+}
+
 TEST(DicomMap, RemoveBinary)
 {
   DicomMap b;
@@ -622,8 +757,7 @@
   {
     ResourceType level = static_cast<ResourceType>(i);
 
-    std::set<DicomTag> tags;
-    DicomMap::GetMainDicomTags(tags, level);
+    const std::set<DicomTag>& tags = DicomMap::GetMainDicomTags(level);
 
     for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
     {
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -357,6 +357,64 @@
 }
 
 
+TEST(FromDcmtkBridge, ParseListOfTags)
+{
+  {// nominal test
+    std::string source = "0010,0010;PatientBirthDate;0020,0020";
+    std::set<DicomTag> result;
+    FromDcmtkBridge::ParseListOfTags(result, source);
+
+    ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_NAME) != result.end());
+    ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_BIRTH_DATE) != result.end());
+    ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ORIENTATION) != result.end());
+    ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ID) == result.end());
+
+    // serialize to string
+    std::string serialized;
+    FromDcmtkBridge::FormatListOfTags(serialized, result);
+    ASSERT_EQ("0010,0010;0010,0030;0020,0020", serialized);
+  }
+
+  {// no tag
+    std::string source = "";
+    std::set<DicomTag> result;
+    FromDcmtkBridge::ParseListOfTags(result, source);
+
+    ASSERT_EQ(0, result.size());
+  }
+
+  {// invalid tag
+    std::string source = "0010,0010;Patient-BirthDate;0020,0020";
+    std::set<DicomTag> result;
+    
+    ASSERT_THROW(FromDcmtkBridge::ParseListOfTags(result, source), OrthancException);
+  }
+
+  {// duplicate tag only once
+    std::string source = "0010,0010;PatientName";
+    std::set<DicomTag> result;
+    
+    FromDcmtkBridge::ParseListOfTags(result, source);
+
+    ASSERT_EQ(1, result.size());
+  }
+
+  {// Json
+    Json::Value source = Json::arrayValue;
+    source.append("0010,0010");
+    source.append("PatientBirthDate");
+    source.append("0020,0020");
+    std::set<DicomTag> result;
+    FromDcmtkBridge::ParseListOfTags(result, source);
+
+    ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_NAME) != result.end());
+    ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_BIRTH_DATE) != result.end());
+    ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ORIENTATION) != result.end());
+    ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ID) == result.end());
+  }
+
+
+}
 
 static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110);
 static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120);
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -209,3 +209,116 @@
   std::unique_ptr<SingleValueObject<int> > j(new SingleValueObject<int>(42));
   ASSERT_EQ(42, j->GetValue());
 }
+
+TEST(Toolbox, IsSetInSet)
+{
+  {
+    std::set<int> needles;
+    std::set<int> haystack;
+    std::set<int> missings;
+
+    ASSERT_TRUE(Toolbox::IsSetInSet<int>(needles, haystack));
+    ASSERT_EQ(0, Toolbox::GetMissingsFromSet<int>(missings, needles, haystack));
+  }
+
+  {
+    std::set<int> needles;
+    std::set<int> haystack;
+    std::set<int> missings;
+
+    haystack.insert(5);
+    ASSERT_TRUE(Toolbox::IsSetInSet<int>(needles, haystack));
+    ASSERT_EQ(0, Toolbox::GetMissingsFromSet<int>(missings, needles, haystack));
+  }
+
+  {
+    std::set<int> needles;
+    std::set<int> haystack;
+    std::set<int> missings;
+
+    needles.insert(5);
+    haystack.insert(5);
+    ASSERT_TRUE(Toolbox::IsSetInSet<int>(needles, haystack));
+    ASSERT_EQ(0, Toolbox::GetMissingsFromSet<int>(missings, needles, haystack));
+  }
+
+  {
+    std::set<int> needles;
+    std::set<int> haystack;
+    std::set<int> missings;
+
+    needles.insert(5);
+    
+    ASSERT_FALSE(Toolbox::IsSetInSet<int>(needles, haystack));
+    ASSERT_EQ(1, Toolbox::GetMissingsFromSet<int>(missings, needles, haystack));
+    ASSERT_TRUE(missings.count(5) == 1);
+  }
+
+  {
+    std::set<int> needles;
+    std::set<int> haystack;
+    std::set<int> missings;
+
+    needles.insert(6);
+    haystack.insert(5);
+    ASSERT_FALSE(Toolbox::IsSetInSet<int>(needles, haystack));
+    ASSERT_EQ(1, Toolbox::GetMissingsFromSet<int>(missings, needles, haystack));
+    ASSERT_TRUE(missings.count(6) == 1);
+  }
+
+  {
+    std::set<int> needles;
+    std::set<int> haystack;
+    std::set<int> missings;
+
+    needles.insert(5);
+    needles.insert(6);
+    haystack.insert(5);
+    haystack.insert(6);
+    ASSERT_TRUE(Toolbox::IsSetInSet<int>(needles, haystack));
+    ASSERT_EQ(0, Toolbox::GetMissingsFromSet<int>(missings, needles, haystack));
+  }
+}
+
+TEST(Toolbox, JoinStrings)
+{
+  {
+    std::set<std::string> source;
+    std::string result;
+
+    Toolbox::JoinStrings(result, source, ";");
+    ASSERT_EQ("", result);
+  }
+
+  {
+    std::set<std::string> source;
+    source.insert("1");
+
+    std::string result;
+
+    Toolbox::JoinStrings(result, source, ";");
+    ASSERT_EQ("1", result);
+  }
+
+  {
+    std::set<std::string> source;
+    source.insert("2");
+    source.insert("1");
+
+    std::string result;
+
+    Toolbox::JoinStrings(result, source, ";");
+    ASSERT_EQ("1;2", result);
+  }
+
+  {
+    std::set<std::string> source;
+    source.insert("2");
+    source.insert("1");
+
+    std::string result;
+
+    Toolbox::JoinStrings(result, source, "\\");
+    ASSERT_EQ("1\\2", result);
+  }
+}
\ No newline at end of file
--- a/OrthancServer/CMakeLists.txt	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/CMakeLists.txt	Mon Apr 25 15:50:57 2022 +0200
@@ -59,6 +59,7 @@
 SET(BUILD_RECOVER_COMPRESSED_FILE ON CACHE BOOL "Whether to build the companion tool to recover files compressed using Orthanc")
 SET(BUILD_SERVE_FOLDERS ON CACHE BOOL "Whether to build the ServeFolders plugin")
 SET(BUILD_CONNECTIVITY_CHECKS ON CACHE BOOL "Whether to build the ConnectivityChecks plugin")
+SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin")
 SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins")
 SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests")
 
@@ -315,6 +316,7 @@
   -DHAS_ORTHANC_EXCEPTION=0
   -DMODALITY_WORKLISTS_VERSION="${ORTHANC_VERSION}"
   -DSERVE_FOLDERS_VERSION="${ORTHANC_VERSION}"
+  -DDB_OPTIMIZER_VERSION="${ORTHANC_VERSION}"
   )
 
 
@@ -427,7 +429,7 @@
 #####################################################################
 
 if (ENABLE_PLUGINS AND
-    (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS))
+    (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_DB_OPTIMIZER))
   add_library(ThirdPartyPlugins STATIC
     ${BOOST_SOURCES}
     ${JSONCPP_SOURCES}
@@ -605,6 +607,50 @@
 
 
 #####################################################################
+## Build the "Housekeeper" plugin
+#####################################################################
+
+if (ENABLE_PLUGINS AND BUILD_DB_OPTIMIZER)
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+execute_process(
+  COMMAND 
+  ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py
+  ${ORTHANC_VERSION} Housekeeper Housekeeper.dll "Sample Orthanc plugin to optimize/clean the DB/Storage"
+  ERROR_VARIABLE Failure
+  OUTPUT_FILE ${AUTOGENERATED_DIR}/Housekeeper.rc
+  )
+
+if (Failure)
+  message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+endif()
+
+list(APPEND HOUSEKEEPER_RESOURCES ${AUTOGENERATED_DIR}/Housekeeper.rc)
+endif()
+
+add_library(Housekeeper SHARED 
+${CMAKE_SOURCE_DIR}/Plugins/Samples/Housekeeper/Plugin.cpp
+${HOUSEKEEPER_RESOURCES}
+)
+
+target_link_libraries(Housekeeper 
+  ThirdPartyPlugins
+  )
+
+set_target_properties(
+  Housekeeper PROPERTIES 
+VERSION ${ORTHANC_VERSION} 
+SOVERSION ${ORTHANC_VERSION}
+)
+
+install(
+TARGETS Housekeeper
+RUNTIME DESTINATION lib    # Destination for Windows
+LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+)
+endif()
+
+
+#####################################################################
 ## Build the companion tool to recover files compressed using Orthanc
 #####################################################################
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Apr 25 15:50:57 2022 +0200
@@ -244,6 +244,7 @@
     OrthancPluginErrorCode_BadRange = 41    /*!< Incorrect range request */,
     OrthancPluginErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
     OrthancPluginErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
+    OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -1546,24 +1546,18 @@
              " is required)");
   }
 
-
-  bool CheckMinimalOrthancVersion(unsigned int major,
-                                  unsigned int minor,
-                                  unsigned int revision)
+  bool CheckMinimalVersion(const char* version,
+                           unsigned int major,
+                           unsigned int minor,
+                           unsigned int revision)
   {
-    if (!HasGlobalContext())
-    {
-      LogError("Bad Orthanc context in the plugin");
-      return false;
-    }
-
-    if (!strcmp(GetGlobalContext()->orthancVersion, "mainline"))
+    if (!strcmp(version, "mainline"))
     {
       // Assume compatibility with the mainline
       return true;
     }
 
-    // Parse the version of the Orthanc core
+    // Parse the version
     int aa, bb, cc;
     if (
 #ifdef _MSC_VER
@@ -1571,7 +1565,7 @@
 #else
       sscanf
 #endif
-      (GetGlobalContext()->orthancVersion, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 ||
+      (version, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 ||
       aa < 0 ||
       bb < 0 ||
       cc < 0)
@@ -1595,7 +1589,6 @@
       return false;
     }
 
-
     // Check the minor version number
     assert(a == major);
 
@@ -1623,6 +1616,21 @@
   }
 
 
+  bool CheckMinimalOrthancVersion(unsigned int major,
+                                  unsigned int minor,
+                                  unsigned int revision)
+  {
+    if (!HasGlobalContext())
+    {
+      LogError("Bad Orthanc context in the plugin");
+      return false;
+    }
+
+    return CheckMinimalVersion(GetGlobalContext()->orthancVersion,
+                               major, minor, revision);
+  }
+
+
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
   const char* AutodetectMimeType(const std::string& path)
   {
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Mon Apr 25 15:50:57 2022 +0200
@@ -303,6 +303,11 @@
       return str_;
     }
 
+    bool IsNullOrEmpty() const
+    {
+      return str_ == NULL || str_[0] == 0;
+    }
+
     void ToString(std::string& target) const;
 
     void ToJson(Json::Value& target) const;
@@ -610,6 +615,10 @@
                                   unsigned int minor,
                                   unsigned int revision);
 
+  bool CheckMinimalVersion(const char* version,
+                           unsigned int major,
+                           unsigned int minor,
+                           unsigned int revision);
 
   namespace Internals
   {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -0,0 +1,679 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../../../OrthancFramework/Sources/Compatibility.h"
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <boost/thread.hpp>
+#include <boost/algorithm/string.hpp>
+#include <json/value.h>
+#include <json/writer.h>
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+#include <map>
+#include <list>
+#include <time.h>
+
+static int globalPropertyId_ = 0;
+static bool force_ = false;
+static uint throttleDelay_ = 0;
+static std::unique_ptr<boost::thread> workerThread_;
+static bool workerThreadShouldStop_ = false;
+static bool triggerOnStorageCompressionChange_ = true;
+static bool triggerOnMainDicomTagsChange_ = true;
+static bool triggerOnUnnecessaryDicomAsJsonFiles_ = true;
+
+
+struct RunningPeriod
+{
+  int fromHour_;
+  int toHour_;
+  int weekday_;
+
+  RunningPeriod(const std::string& weekday, const std::string& period)
+  {
+    if (weekday == "Monday")
+    {
+      weekday_ = 1;
+    }
+    else if (weekday == "Tuesday")
+    {
+      weekday_ = 2;
+    }
+    else if (weekday == "Wednesday")
+    {
+      weekday_ = 3;
+    }
+    else if (weekday == "Thursday")
+    {
+      weekday_ = 4;
+    }
+    else if (weekday == "Friday")
+    {
+      weekday_ = 5;
+    }
+    else if (weekday == "Saturday")
+    {
+      weekday_ = 6;
+    }
+    else if (weekday == "Sunday")
+    {
+      weekday_ = 0;
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Housekeeper: invalid schedule: unknown 'day': " + weekday);      
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+
+    std::vector<std::string> hours;
+    boost::split(hours, period, boost::is_any_of("-"));
+
+    fromHour_ = boost::lexical_cast<int>(hours[0]);
+    toHour_ = boost::lexical_cast<int>(hours[1]);
+  }
+
+  bool isInPeriod() const
+  {
+    time_t now = time(NULL);
+    tm* nowLocalTime = localtime(&now);
+
+    if (nowLocalTime->tm_wday != weekday_)
+    {
+      return false;
+    }
+
+    if (nowLocalTime->tm_hour >= fromHour_ && nowLocalTime->tm_hour < toHour_)
+    {
+      return true;
+    }
+
+    return false;
+  }
+};
+
+struct RunningPeriods
+{
+  std::list<RunningPeriod> runningPeriods_;
+
+  void load(const Json::Value& scheduleConfiguration)
+  {
+//   "Monday": ["0-6", "20-24"],
+
+    Json::Value::Members names = scheduleConfiguration.getMemberNames();
+
+    for (Json::Value::Members::const_iterator it = names.begin();
+      it != names.end(); it++)
+    {
+      for (Json::Value::ArrayIndex i = 0; i < scheduleConfiguration[*it].size(); i++)
+      {
+        runningPeriods_.push_back(RunningPeriod(*it, scheduleConfiguration[*it][i].asString()));
+      }
+    }
+  }
+
+  bool isInPeriod()
+  {
+    if (runningPeriods_.size() == 0)
+    {
+      return true;  // if no config: always run
+    }
+
+    for (std::list<RunningPeriod>::const_iterator it = runningPeriods_.begin();
+      it != runningPeriods_.end(); it++)
+    {
+      if (it->isInPeriod())
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+};
+
+RunningPeriods runningPeriods_;
+
+struct DbConfiguration
+{
+  std::string orthancVersion;
+  std::map<OrthancPluginResourceType, std::string> mainDicomTagsSignature;
+  bool storageCompressionEnabled;
+
+  DbConfiguration()
+  : storageCompressionEnabled(false)
+  {
+  }
+
+  bool IsDefined() const
+  {
+    return !orthancVersion.empty() && mainDicomTagsSignature.size() == 4;
+  }
+
+  void Clear()
+  {
+    orthancVersion.clear();
+    mainDicomTagsSignature.clear();
+  }
+
+  void ToJson(Json::Value& target)
+  {
+    if (!IsDefined())
+    {
+      target = Json::nullValue;
+    }
+    else
+    {
+      Json::Value signatures;
+
+      target = Json::objectValue;
+
+      // default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed):
+      signatures["Patient"] = mainDicomTagsSignature[OrthancPluginResourceType_Patient];
+      signatures["Study"] = mainDicomTagsSignature[OrthancPluginResourceType_Study];
+      signatures["Series"] = mainDicomTagsSignature[OrthancPluginResourceType_Series];
+      signatures["Instance"] = mainDicomTagsSignature[OrthancPluginResourceType_Instance];
+
+      target["MainDicomTagsSignature"] = signatures;
+      target["OrthancVersion"] = orthancVersion;
+      target["StorageCompressionEnabled"] = storageCompressionEnabled;
+    }
+  }
+
+  void FromJson(Json::Value& source)
+  {
+    if (!source.isNull())
+    {
+      orthancVersion = source["OrthancVersion"].asString();
+
+      const Json::Value& signatures = source["MainDicomTagsSignature"];
+      mainDicomTagsSignature[OrthancPluginResourceType_Patient] = signatures["Patient"].asString();
+      mainDicomTagsSignature[OrthancPluginResourceType_Study] = signatures["Study"].asString();
+      mainDicomTagsSignature[OrthancPluginResourceType_Series] = signatures["Series"].asString();
+      mainDicomTagsSignature[OrthancPluginResourceType_Instance] = signatures["Instance"].asString();
+
+      storageCompressionEnabled = source["StorageCompressionEnabled"].asBool();
+    }
+  }
+};
+
+struct PluginStatus
+{
+  int statusVersion;
+  int64_t lastProcessedChange;
+  int64_t lastChangeToProcess;
+
+  DbConfiguration currentlyProcessingConfiguration; // last configuration being processed (has not reached last change yet)
+  DbConfiguration lastProcessedConfiguration;       // last configuration that has been fully processed (till last change)
+
+  PluginStatus()
+  : statusVersion(1),
+    lastProcessedChange(-1),
+    lastChangeToProcess(-1)
+  {
+  }
+
+  void ToJson(Json::Value& target)
+  {
+    target = Json::objectValue;
+
+    target["Version"] = statusVersion;
+    target["LastProcessedChange"] = Json::Value::Int64(lastProcessedChange);
+    target["LastChangeToProcess"] = Json::Value::Int64(lastChangeToProcess);
+
+    currentlyProcessingConfiguration.ToJson(target["CurrentlyProcessingConfiguration"]);
+    lastProcessedConfiguration.ToJson(target["LastProcessedConfiguration"]);
+  }
+
+  void FromJson(Json::Value& source)
+  {
+    statusVersion = source["Version"].asInt();
+    lastProcessedChange = source["LastProcessedChange"].asInt64();
+    lastChangeToProcess = source["LastChangeToProcess"].asInt64();
+
+    Json::Value& current = source["CurrentlyProcessingConfiguration"];
+    Json::Value& last = source["LastProcessedConfiguration"];
+
+    currentlyProcessingConfiguration.FromJson(current);
+    lastProcessedConfiguration.FromJson(last);
+  }
+};
+
+
+static void ReadStatusFromDb(PluginStatus& pluginStatus)
+{
+  OrthancPlugins::OrthancString globalPropertyContent;
+
+  globalPropertyContent.Assign(OrthancPluginGetGlobalProperty(OrthancPlugins::GetGlobalContext(),
+                                                              globalPropertyId_,
+                                                              ""));
+
+  if (!globalPropertyContent.IsNullOrEmpty())
+  {
+    Json::Value jsonStatus;
+    globalPropertyContent.ToJson(jsonStatus);
+    pluginStatus.FromJson(jsonStatus);
+  }
+  else
+  {
+    // default config
+    pluginStatus.statusVersion = 1;
+    pluginStatus.lastProcessedChange = -1;
+    pluginStatus.lastChangeToProcess = -1;
+    
+    pluginStatus.currentlyProcessingConfiguration.orthancVersion = "1.9.0"; // when we don't know, we assume some files were stored with Orthanc 1.9.0 (last version saving the dicom-as-json files)
+
+    // default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed):
+    pluginStatus.currentlyProcessingConfiguration.mainDicomTagsSignature[OrthancPluginResourceType_Patient] = "0010,0010;0010,0020;0010,0030;0010,0040;0010,1000";
+    pluginStatus.currentlyProcessingConfiguration.mainDicomTagsSignature[OrthancPluginResourceType_Study] = "0008,0020;0008,0030;0008,0050;0008,0080;0008,0090;0008,1030;0020,000d;0020,0010;0032,1032;0032,1060";
+    pluginStatus.currentlyProcessingConfiguration.mainDicomTagsSignature[OrthancPluginResourceType_Series] = "0008,0021;0008,0031;0008,0060;0008,0070;0008,1010;0008,103e;0008,1070;0018,0010;0018,0015;0018,0024;0018,1030;0018,1090;0018,1400;0020,000e;0020,0011;0020,0037;0020,0105;0020,1002;0040,0254;0054,0081;0054,0101;0054,1000";
+    pluginStatus.currentlyProcessingConfiguration.mainDicomTagsSignature[OrthancPluginResourceType_Instance] = "0008,0012;0008,0013;0008,0018;0020,0012;0020,0013;0020,0032;0020,0037;0020,0100;0020,4000;0028,0008;0054,1330"; 
+  }
+}
+
+static void SaveStatusInDb(PluginStatus& pluginStatus)
+{
+  Json::Value jsonStatus;
+  pluginStatus.ToJson(jsonStatus);
+
+  Json::StreamWriterBuilder builder;
+  builder.settings_["indentation"] = "   ";
+  std::string serializedStatus = Json::writeString(builder, jsonStatus);
+
+  OrthancPluginSetGlobalProperty(OrthancPlugins::GetGlobalContext(),
+                                 globalPropertyId_,
+                                 serializedStatus.c_str());
+}
+
+static void GetCurrentDbConfiguration(DbConfiguration& configuration)
+{
+  Json::Value signatures;
+  Json::Value systemInfo;
+
+  OrthancPlugins::RestApiGet(systemInfo, "/system", false);
+  configuration.mainDicomTagsSignature[OrthancPluginResourceType_Patient] = systemInfo["MainDicomTags"]["Patient"].asString();
+  configuration.mainDicomTagsSignature[OrthancPluginResourceType_Study] = systemInfo["MainDicomTags"]["Study"].asString();
+  configuration.mainDicomTagsSignature[OrthancPluginResourceType_Series] = systemInfo["MainDicomTags"]["Series"].asString();
+  configuration.mainDicomTagsSignature[OrthancPluginResourceType_Instance] = systemInfo["MainDicomTags"]["Instance"].asString();
+  configuration.storageCompressionEnabled = systemInfo["StorageCompression"].asBool();
+
+  configuration.orthancVersion = OrthancPlugins::GetGlobalContext()->orthancVersion;
+}
+
+static bool NeedsProcessing(const DbConfiguration& current, const DbConfiguration& last)
+{
+  if (!last.IsDefined())
+  {
+    return true;
+  }
+
+  const char* lastVersion = last.orthancVersion.c_str();
+  const std::map<OrthancPluginResourceType, std::string>& lastTags = last.mainDicomTagsSignature;
+  const std::map<OrthancPluginResourceType, std::string>& currentTags = current.mainDicomTagsSignature;
+  bool needsProcessing = false;
+
+  if (!OrthancPlugins::CheckMinimalVersion(lastVersion, 1, 9, 1))
+  {
+    if (triggerOnUnnecessaryDicomAsJsonFiles_)
+    {
+      OrthancPlugins::LogWarning("Housekeeper: your storage might still contain some dicom-as-json files -> will perform housekeeping");
+      needsProcessing = true;
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Housekeeper: your storage might still contain some dicom-as-json files but the trigger has been disabled");
+    }
+  }
+
+  if (lastTags.at(OrthancPluginResourceType_Patient) != currentTags.at(OrthancPluginResourceType_Patient))
+  {
+    if (triggerOnMainDicomTagsChange_)
+    {
+      OrthancPlugins::LogWarning("Housekeeper: Patient main dicom tags have changed, -> will perform housekeeping");
+      needsProcessing = true;
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Housekeeper: Patient main dicom tags have changed but the trigger is disabled");
+    }
+  }
+
+  if (lastTags.at(OrthancPluginResourceType_Study) != currentTags.at(OrthancPluginResourceType_Study))
+  {
+    if (triggerOnMainDicomTagsChange_)
+    {
+      OrthancPlugins::LogWarning("Housekeeper: Study main dicom tags have changed, -> will perform housekeeping");
+      needsProcessing = true;
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Housekeeper: Study main dicom tags have changed but the trigger is disabled");
+    }
+  }
+
+  if (lastTags.at(OrthancPluginResourceType_Series) != currentTags.at(OrthancPluginResourceType_Series))
+  {
+    if (triggerOnMainDicomTagsChange_)
+    {
+      OrthancPlugins::LogWarning("Housekeeper: Series main dicom tags have changed, -> will perform housekeeping");
+      needsProcessing = true;
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Housekeeper: Series main dicom tags have changed but the trigger is disabled");
+    }
+  }
+
+  if (lastTags.at(OrthancPluginResourceType_Instance) != currentTags.at(OrthancPluginResourceType_Instance))
+  {
+    if (triggerOnMainDicomTagsChange_)
+    {
+      OrthancPlugins::LogWarning("Housekeeper: Instance main dicom tags have changed, -> will perform housekeeping");
+      needsProcessing = true;
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Housekeeper: Instance main dicom tags have changed but the trigger is disabled");
+    }
+  }
+
+  if (current.storageCompressionEnabled != last.storageCompressionEnabled)
+  {
+    if (triggerOnStorageCompressionChange_)
+    {
+      if (current.storageCompressionEnabled)
+      {
+        OrthancPlugins::LogWarning("Housekeeper: storage compression is now enabled -> will perform housekeeping");
+      }
+      else
+      {
+        OrthancPlugins::LogWarning("Housekeeper: storage compression is now disabled -> will perform housekeeping");
+      }
+      
+      needsProcessing = true;
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Housekeeper: storage compression has changed but the trigger is disabled");
+    }
+  }
+
+  return needsProcessing;
+}
+
+static bool ProcessChanges(PluginStatus& pluginStatus, const DbConfiguration& currentDbConfiguration)
+{
+  Json::Value changes;
+
+  pluginStatus.currentlyProcessingConfiguration = currentDbConfiguration;
+
+  OrthancPlugins::RestApiGet(changes, "/changes?since=" + boost::lexical_cast<std::string>(pluginStatus.lastProcessedChange) + "&limit=100", false);
+
+  for (Json::ArrayIndex i = 0; i < changes["Changes"].size(); i++)
+  {
+    const Json::Value& change = changes["Changes"][i];
+    int64_t seq = change["Seq"].asInt64();
+
+    if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
+    {
+      Json::Value result;
+      OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", std::string(""), false);
+      boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 1000));
+    }
+
+    if (seq >= pluginStatus.lastChangeToProcess)  // we are done !
+    {
+      return true;
+    }
+
+    pluginStatus.lastProcessedChange = seq;
+  }
+
+  return false;
+}
+
+
+static void WorkerThread()
+{
+  PluginStatus pluginStatus;
+  DbConfiguration currentDbConfiguration;
+
+  OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Starting Housekeeper worker thread");
+
+  ReadStatusFromDb(pluginStatus);
+  GetCurrentDbConfiguration(currentDbConfiguration);
+
+  if (!NeedsProcessing(currentDbConfiguration, pluginStatus.lastProcessedConfiguration))
+  {
+    OrthancPlugins::LogWarning("Housekeeper: everything has been processed already !");
+    return;
+  }
+
+  if (force_ || NeedsProcessing(currentDbConfiguration, pluginStatus.currentlyProcessingConfiguration))
+  {
+    if (force_)
+    {
+      OrthancPlugins::LogWarning("Housekeeper: forcing execution -> will perform housekeeping");
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Housekeeper: the DB configuration has changed since last run, will reprocess the whole DB !");
+    }
+    
+    Json::Value changes;
+    OrthancPlugins::RestApiGet(changes, "/changes?last", false);
+
+    pluginStatus.lastProcessedChange = 0;
+    pluginStatus.lastChangeToProcess = changes["Last"].asInt64();  // the last change is the last change at the time we start.  We assume that every new ingested file will be constructed correctly
+  }
+  else
+  {
+    OrthancPlugins::LogWarning("Housekeeper: the DB configuration has not changed since last run, will continue processing changes");
+  }
+
+  bool completed = pluginStatus.lastChangeToProcess == 0;  // if the DB is empty at start, no need to process anyting
+  bool loggedNotRightPeriodChangeMessage = false;
+
+  while (!workerThreadShouldStop_ && !completed)
+  {
+    if (runningPeriods_.isInPeriod())
+    {
+      completed = ProcessChanges(pluginStatus, currentDbConfiguration);
+      SaveStatusInDb(pluginStatus);
+      
+      if (!completed)
+      {
+        OrthancPlugins::LogInfo("Housekeeper: processed changes " + 
+                                boost::lexical_cast<std::string>(pluginStatus.lastProcessedChange) + 
+                                " / " + boost::lexical_cast<std::string>(pluginStatus.lastChangeToProcess));
+        
+        boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 100));  // wait 1/10 of the delay between changes
+      }
+
+      loggedNotRightPeriodChangeMessage = false;
+    }
+    else
+    {
+      if (!loggedNotRightPeriodChangeMessage)
+      {
+        OrthancPlugins::LogInfo("Housekeeper: entering quiet period");
+        loggedNotRightPeriodChangeMessage = true;
+      }
+    }
+  }  
+
+  if (completed)
+  {
+    pluginStatus.lastProcessedConfiguration = currentDbConfiguration;
+    pluginStatus.currentlyProcessingConfiguration.Clear();
+
+    pluginStatus.lastProcessedChange = -1;
+    pluginStatus.lastChangeToProcess = -1;
+    
+    SaveStatusInDb(pluginStatus);
+
+    OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Housekeeper: finished processing all changes");
+  }
+}
+
+extern "C"
+{
+  OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
+                                          OrthancPluginResourceType resourceType,
+                                          const char* resourceId)
+  {
+    switch (changeType)
+    {
+      case OrthancPluginChangeType_OrthancStarted:
+      {
+        workerThread_.reset(new boost::thread(WorkerThread));
+        return OrthancPluginErrorCode_Success;
+      }
+      case OrthancPluginChangeType_OrthancStopped:
+      {
+        if (workerThread_ && workerThread_->joinable())
+        {
+          workerThreadShouldStop_ = true;
+          workerThread_->join();
+        }
+      }
+      default:
+        return OrthancPluginErrorCode_Success;
+    }
+  }
+
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    OrthancPlugins::SetGlobalContext(c);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      return -1;
+    }
+
+    OrthancPlugins::LogWarning("Housekeeper plugin is initializing");
+    OrthancPluginSetDescription(c, "Optimizes your DB and storage.");
+
+    OrthancPlugins::OrthancConfiguration configuration;
+
+    OrthancPlugins::OrthancConfiguration housekeeper;
+    configuration.GetSection(housekeeper, "Housekeeper");
+
+    bool enabled = housekeeper.GetBooleanValue("Enable", false);
+    if (enabled)
+    {
+      /*
+        {
+          "Housekeeper": {
+            
+            // Enables/disables the plugin
+            "Enable": false,
+
+            // the Global Prooperty ID in which the plugin progress
+            // is stored.  Must be > 1024 and must not be used by
+            // another plugin
+            "GlobalPropertyId": 1025,
+
+            // Forces execution even if the plugin did not detect
+            // any changes in configuration
+            "Force": false,
+
+            // Delay (in seconds) between reconstruction of 2 studies
+            // This avoids overloading Orthanc with the housekeeping
+            // process and leaves room for other operations.
+            "ThrottleDelay": 5,
+
+            // Runs the plugin only at certain period of time.
+            // If not specified, the plugin runs all the time
+            // Examples: 
+            // to run between 0AM and 6AM everyday + every night 
+            // from 8PM to 12PM and 24h a day on the weekend:
+            // "Schedule": {
+            //   "Monday": ["0-6", "20-24"],
+            //   "Tuesday": ["0-6", "20-24"],
+            //   "Wednesday": ["0-6", "20-24"],
+            //   "Thursday": ["0-6", "20-24"],
+            //   "Friday": ["0-6", "20-24"],
+            //   "Saturday": ["0-24"],
+            //   "Sunday": ["0-24"]
+            // },
+
+            // configure events that can trigger a housekeeping processing 
+            "Triggers" : {
+              "StorageCompressionChange": true,
+              "MainDicomTagsChange": true,
+              "UnnecessaryDicomAsJsonFiles": true
+            }
+
+          }
+        }
+      */
+
+
+      globalPropertyId_ = housekeeper.GetIntegerValue("GlobalPropertyId", 1025);
+      force_ = housekeeper.GetBooleanValue("Force", false);
+      throttleDelay_ = housekeeper.GetUnsignedIntegerValue("ThrottleDelay", 5);      
+
+      if (housekeeper.GetJson().isMember("Triggers"))
+      {
+        triggerOnStorageCompressionChange_ = housekeeper.GetBooleanValue("StorageCompressionChange", true);
+        triggerOnMainDicomTagsChange_ = housekeeper.GetBooleanValue("MainDicomTagsChange", true);
+        triggerOnUnnecessaryDicomAsJsonFiles_ = housekeeper.GetBooleanValue("UnnecessaryDicomAsJsonFiles", true);
+      }
+
+      if (housekeeper.GetJson().isMember("Schedule"))
+      {
+        runningPeriods_.load(housekeeper.GetJson()["Schedule"]);
+      }
+
+      OrthancPluginRegisterOnChangeCallback(c, OnChangeCallback);
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Housekeeper plugin is disabled by the configuration file");
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPlugins::LogWarning("Housekeeper plugin is finalizing");
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "housekeeper";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return DB_OPTIMIZER_VERSION;
+  }
+}
--- a/OrthancServer/Resources/Configuration.json	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Resources/Configuration.json	Mon Apr 25 15:50:57 2022 +0200
@@ -858,5 +858,49 @@
   // (default behaviour).  A value > 1 is meaningful only if the storage
   // is a distributed network storage (e.g object storage plugin).
   // (new experimental feature in Orthanc 1.10.0)
-  "ZipLoaderThreads": 0
+  "ZipLoaderThreads": 0,
+
+  // Extra Main Dicom tags that are stored in DB together with all default
+  // Main Dicom tags that are already stored (TODO: see book new page). 
+  // (new in Orthanc 1.11.0)
+  // Sequences tags are not supported.
+  /**
+  "ExtraMainDicomTags" : {
+    "Instance" : [
+      "Rows",
+      "Columns",
+      "ImageType",
+      "SOPClassUID",
+      "ContentDate",
+      "ContentTime",
+      "FrameOfReferenceUID",
+      "PixelSpacing",
+      "SpecificCharacterSet",
+      "BitsAllocated"
+    ],
+    "Series" : [],
+    "Study": [],
+    "Patient": []
+  },
+  */
+
+  // Enables/disables warnings in the logs.
+  // "true" enables a warning.  All warnings are enabled by default
+  // TODO: see book new page
+  // (new in Orthanc 1.11.0)
+  "Warnings" : {
+    // A "RequestedTags" has been read from storage which is slower than
+    // reading it from DB.
+    // You might want to store this tag in ExtraMainDicomTags to build
+    // the response faster.
+    "W001_TagsBeingReadFromStorage": true,
+    
+    // Retrieving a list of Main dicom tags from a resource that has been
+    // saved with another "ExtraMainDicomTags" configuration which means that
+    // your response might be incomplete/inconsistent.
+    // You should call patients|studies|series|instances/../reconstruct to rebuild
+    // the DB.  TODO: also check for "rebuild DB" plugin
+    "W002_InconsistentDicomTagsInDb": true
+  }
+
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -362,8 +362,7 @@
       }
 
       {
-        std::set<DicomTag> tags;
-        DicomMap::GetMainDicomTags(tags, level);
+        const std::set<DicomTag>& tags = DicomMap::GetMainDicomTags(level);
 
         for (std::set<DicomTag>::const_iterator
                tag = tags.begin(); tag != tags.end(); ++tag)
@@ -711,46 +710,16 @@
   }
   
 
-  bool StatelessDatabaseOperations::ExpandResource(Json::Value& target,
+  bool StatelessDatabaseOperations::ExpandResource(ExpandedResource& target,
                                                    const std::string& publicId,
                                                    ResourceType level,
-                                                   DicomToJsonFormat format)
+                                                   const std::set<DicomTag>& requestedTags,
+                                                   ExpandResourceDbFlags expandFlags)
   {    
-    class Operations : public ReadOnlyOperationsT5<
-      bool&, Json::Value&, const std::string&, ResourceType, DicomToJsonFormat>
+    class Operations : public ReadOnlyOperationsT6<
+      bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, ExpandResourceDbFlags>
     {
     private:
-      static void MainDicomTagsToJson(ReadOnlyTransaction& transaction,
-                                      Json::Value& target,
-                                      int64_t resourceId,
-                                      ResourceType resourceType,
-                                      DicomToJsonFormat format)
-      {
-        static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
-        static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
-        
-        DicomMap tags;
-        transaction.GetMainDicomTags(tags, resourceId);
-
-        if (resourceType == ResourceType_Study)
-        {
-          DicomMap t1, t2;
-          tags.ExtractStudyInformation(t1);
-          tags.ExtractPatientInformation(t2);
-
-          target[MAIN_DICOM_TAGS] = Json::objectValue;
-          FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], t1, format);
-
-          target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
-          FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], t2, format);
-        }
-        else
-        {
-          target[MAIN_DICOM_TAGS] = Json::objectValue;
-          FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], tags, format);
-        }
-      }
-
   
       static bool LookupStringMetadata(std::string& result,
                                        const std::map<MetadataType, std::string>& metadata,
@@ -797,7 +766,7 @@
                               const Tuple& tuple) ORTHANC_OVERRIDE
       {
         // Lookup for the requested resource
-        int64_t internalId;  // unused
+        int64_t internalId;
         ResourceType type;
         std::string parent;
         if (!transaction.LookupResourceAndParent(internalId, type, parent, tuple.get<2>()) ||
@@ -807,9 +776,9 @@
         }
         else
         {
-          Json::Value& target = tuple.get<1>();
-          target = Json::objectValue;
-        
+          ExpandedResource& target = tuple.get<1>();
+          ExpandResourceDbFlags expandFlags = tuple.get<5>();
+
           // Set information about the parent resource (if it exists)
           if (type == ResourceType_Patient)
           {
@@ -825,150 +794,166 @@
               throw OrthancException(ErrorCode_DatabasePlugin);
             }
 
-            switch (type)
-            {
-              case ResourceType_Study:
-                target["ParentPatient"] = parent;
-                break;
-
-              case ResourceType_Series:
-                target["ParentStudy"] = parent;
-                break;
-
-              case ResourceType_Instance:
-                target["ParentSeries"] = parent;
-                break;
-
-              default:
-                throw OrthancException(ErrorCode_InternalError);
-            }
+            target.parentId_ = parent;
           }
 
-          // List the children resources
-          std::list<std::string> children;
-          transaction.GetChildrenPublicId(children, internalId);
-
-          if (type != ResourceType_Instance)
+          target.type_ = type;
+          target.id_ = tuple.get<2>();
+
+          if (expandFlags & ExpandResourceDbFlags_IncludeChildren)
           {
-            Json::Value c = Json::arrayValue;
-
-            for (std::list<std::string>::const_iterator
-                   it = children.begin(); it != children.end(); ++it)
-            {
-              c.append(*it);
-            }
+            // List the children resources
+            transaction.GetChildrenPublicId(target.childrenIds_, internalId);
+          }
+
+          if (expandFlags & ExpandResourceDbFlags_IncludeMetadata)
+          {
+            // Extract the metadata
+            transaction.GetAllMetadata(target.metadata_, internalId);
 
             switch (type)
             {
               case ResourceType_Patient:
-                target["Studies"] = c;
-                break;
-
               case ResourceType_Study:
-                target["Series"] = c;
                 break;
 
               case ResourceType_Series:
-                target["Instances"] = c;
+              {
+                int64_t i;
+                if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Series_ExpectedNumberOfInstances))
+                {
+                  target.expectedNumberOfInstances_ = static_cast<int>(i);
+                  target.status_ = EnumerationToString(transaction.GetSeriesStatus(internalId, i));
+                }
+                else
+                {
+                  target.expectedNumberOfInstances_ = -1;
+                  target.status_ = EnumerationToString(SeriesStatus_Unknown);
+                }
+
                 break;
+              }
+
+              case ResourceType_Instance:
+              {
+                FileInfo attachment;
+                int64_t revision;  // ignored
+                if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom))
+                {
+                  throw OrthancException(ErrorCode_InternalError);
+                }
+
+                target.fileSize_ = static_cast<unsigned int>(attachment.GetUncompressedSize());
+                target.fileUuid_ = attachment.GetUuid();
+
+                int64_t i;
+                if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Instance_IndexInSeries))
+                {
+                  target.indexInSeries_ = static_cast<int>(i);
+                }
+                else
+                {
+                  target.indexInSeries_ = -1;
+                }
+
+                break;
+              }
 
               default:
                 throw OrthancException(ErrorCode_InternalError);
             }
+
+            // check the main dicom tags list has not changed since the resource was stored
+            target.mainDicomTagsSignature_ = DicomMap::GetDefaultMainDicomTagsSignature(type);
+            LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature);
           }
 
-          // Extract the metadata
-          std::map<MetadataType, std::string> metadata;
-          transaction.GetAllMetadata(metadata, internalId);
-
-          // Set the resource type
-          switch (type)
+          if (expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags)
           {
-            case ResourceType_Patient:
-              target["Type"] = "Patient";
-              break;
-
-            case ResourceType_Study:
-              target["Type"] = "Study";
-              break;
-
-            case ResourceType_Series:
+            // read all tags from DB
+            transaction.GetMainDicomTags(target.tags_, internalId);
+
+            // check if we have access to all requestedTags or if we must get tags from parents
+            const std::set<DicomTag>& requestedTags = tuple.get<4>();
+
+            if (requestedTags.size() > 0)
             {
-              target["Type"] = "Series";
-
-              int64_t i;
-              if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances))
+              std::set<DicomTag> savedMainDicomTags;
+              
+              FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, target.mainDicomTagsSignature_);
+
+              // read parent main dicom tags as long as we don't have gathered all requested tags
+              ResourceType currentLevel = target.type_;
+              int64_t currentInternalId = internalId;
+              Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
+
+              while ((target.missingRequestedTags_.size() > 0)
+                    && currentLevel != ResourceType_Patient)
               {
-                target["ExpectedNumberOfInstances"] = static_cast<int>(i);
-                target["Status"] = EnumerationToString(transaction.GetSeriesStatus(internalId, i));
+                currentLevel = GetParentResourceType(currentLevel);
+
+                int64_t currentParentId;
+                if (!transaction.LookupParent(currentParentId, currentInternalId))
+                {
+                  break;
+                }
+
+                std::map<MetadataType, std::string> parentMetadata;
+                transaction.GetAllMetadata(parentMetadata, currentParentId);
+
+                std::string parentMainDicomTagsSignature = DicomMap::GetDefaultMainDicomTagsSignature(currentLevel);
+                LookupStringMetadata(parentMainDicomTagsSignature, parentMetadata, MetadataType_MainDicomTagsSignature);
+
+                std::set<DicomTag> parentSavedMainDicomTags;
+                FromDcmtkBridge::ParseListOfTags(parentSavedMainDicomTags, parentMainDicomTagsSignature);
+                
+                size_t previousMissingCount = target.missingRequestedTags_.size();
+                Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags);
+                Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
+
+                // read the parent tags from DB only if it reduces the number of missing tags
+                if (target.missingRequestedTags_.size() < previousMissingCount)
+                { 
+                  Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags);
+
+                  DicomMap parentTags;
+                  transaction.GetMainDicomTags(parentTags, currentParentId);
+
+                  target.tags_.Merge(parentTags);
+                }
+
+                currentInternalId = currentParentId;
               }
-              else
-              {
-                target["ExpectedNumberOfInstances"] = Json::nullValue;
-                target["Status"] = EnumerationToString(SeriesStatus_Unknown);
-              }
-
-              break;
             }
-
-            case ResourceType_Instance:
-            {
-              target["Type"] = "Instance";
-
-              FileInfo attachment;
-              int64_t revision;  // ignored
-              if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom))
-              {
-                throw OrthancException(ErrorCode_InternalError);
-              }
-
-              target["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize());
-              target["FileUuid"] = attachment.GetUuid();
-
-              int64_t i;
-              if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries))
-              {
-                target["IndexInSeries"] = static_cast<int>(i);
-              }
-              else
-              {
-                target["IndexInSeries"] = Json::nullValue;
-              }
-
-              break;
-            }
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
           }
 
-          // Record the remaining information
-          target["ID"] = tuple.get<2>();
-          MainDicomTagsToJson(transaction, target, internalId, type, tuple.get<4>());
-
           std::string tmp;
 
-          if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom))
+          if (LookupStringMetadata(tmp, target.metadata_, MetadataType_AnonymizedFrom))
           {
-            target["AnonymizedFrom"] = tmp;
+            target.anonymizedFrom_ = tmp;
           }
 
-          if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom))
+          if (LookupStringMetadata(tmp, target.metadata_, MetadataType_ModifiedFrom))
           {
-            target["ModifiedFrom"] = tmp;
+            target.modifiedFrom_ = tmp;
           }
 
           if (type == ResourceType_Patient ||
               type == ResourceType_Study ||
               type == ResourceType_Series)
           {
-            target["IsStable"] = !transaction.GetTransactionContext().IsUnstableResource(internalId);
-
-            if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate))
+            target.isStable_ = !transaction.GetTransactionContext().IsUnstableResource(internalId);
+
+            if (LookupStringMetadata(tmp, target.metadata_, MetadataType_LastUpdate))
             {
-              target["LastUpdate"] = tmp;
+              target.lastUpdate_ = tmp;
             }
           }
+          else
+          {
+            target.isStable_ = false;
+          }
 
           tuple.get<0>() = true;
         }
@@ -977,7 +962,7 @@
 
     bool found;
     Operations operations;
-    operations.Apply(*this, found, target, publicId, level, format);
+    operations.Apply(*this, found, target, publicId, level, requestedTags, expandFlags);
     return found;
   }
 
@@ -2701,7 +2686,13 @@
           content.AddResource(study, ResourceType_Study, summary_);
           content.AddResource(series, ResourceType_Series, summary_);
           content.AddResource(instance, ResourceType_Instance, summary_);
+
           transaction.SetResourcesContent(content);
+
+          ReplaceMetadata(transaction, patient, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));    // New in Orthanc 1.11.0
+          ReplaceMetadata(transaction, study, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));        // New in Orthanc 1.11.0
+          ReplaceMetadata(transaction, series, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));      // New in Orthanc 1.11.0
+          ReplaceMetadata(transaction, instance, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
         }
 
         if (hasTransferSyntax_)
@@ -2716,6 +2707,7 @@
         {
           ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent());
         }
+
       }
     };
 
@@ -3082,20 +3074,24 @@
             // Populate the tags of the newly-created resources
 
             content.AddResource(instanceId, ResourceType_Instance, dicomSummary_);
+            content.AddMetadata(instanceId, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
 
             if (status.isNewSeries_)
             {
               content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_);
+              content.AddMetadata(status.seriesId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));  // New in Orthanc 1.11.0
             }
 
             if (status.isNewStudy_)
             {
               content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_);
+              content.AddMetadata(status.studyId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));  // New in Orthanc 1.11.0
             }
 
             if (status.isNewPatient_)
             {
               content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_);
+              content.AddMetadata(status.patientId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));  // New in Orthanc 1.11.0
             }
 
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Apr 25 15:50:57 2022 +0200
@@ -37,6 +37,45 @@
   class ParsedDicomFile;
   struct ServerIndexChange;
 
+  struct ExpandedResource : public boost::noncopyable
+  {
+    std::string                         id_;
+    DicomMap                            tags_;  // all tags from DB
+    std::string                         mainDicomTagsSignature_;
+    std::string                         parentId_;
+    std::list<std::string>              childrenIds_;
+    std::map<MetadataType, std::string> metadata_;
+    ResourceType                        type_;
+    std::string                         anonymizedFrom_;
+    std::string                         modifiedFrom_;
+    std::string                         lastUpdate_;
+    std::set<DicomTag>                  missingRequestedTags_;
+
+    // for patients/studies/series
+    bool                                isStable_;
+
+    // for series only
+    int                                 expectedNumberOfInstances_;
+    std::string                         status_;
+
+    // for instances only
+    size_t                              fileSize_;
+    std::string                         fileUuid_;
+    int                                 indexInSeries_;
+  };
+
+  enum ExpandResourceDbFlags
+  {
+    ExpandResourceDbFlags_None                    = 0,
+    ExpandResourceDbFlags_IncludeMetadata         = (1 << 0),
+    ExpandResourceDbFlags_IncludeChildren         = (1 << 1),
+    ExpandResourceDbFlags_IncludeMainDicomTags    = (1 << 2),
+
+    ExpandResourceDbFlags_Default = (ExpandResourceDbFlags_IncludeMetadata |
+                                     ExpandResourceDbFlags_IncludeChildren |
+                                     ExpandResourceDbFlags_IncludeMainDicomTags)
+  };
+
   class StatelessDatabaseOperations : public boost::noncopyable
   {
   public:
@@ -448,10 +487,11 @@
   
     void Apply(IReadWriteOperations& operations);
 
-    bool ExpandResource(Json::Value& target,
+    bool ExpandResource(ExpandedResource& target,
                         const std::string& publicId,
                         ResourceType level,
-                        DicomToJsonFormat format);
+                        const std::set<DicomTag>& requestedTags,
+                        ExpandResourceDbFlags expandFlags);
 
     void GetAllMetadata(std::map<MetadataType, std::string>& target,
                         const std::string& publicId,
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -43,6 +43,7 @@
 static const char* const ORTHANC_PEERS_IN_DB = "OrthancPeersInDatabase";
 static const char* const TEMPORARY_DIRECTORY = "TemporaryDirectory";
 static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier";
+static const char* const WARNINGS = "Warnings";
 
 namespace Orthanc
 {
@@ -1055,7 +1056,51 @@
     }
   }
 
-  
+  void OrthancConfiguration::LoadWarnings()
+  {
+    if (json_.isMember(WARNINGS))
+    {
+      const Json::Value& warnings = json_[WARNINGS];
+      if (!warnings.isObject())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, std::string(WARNINGS) + " configuration entry is not a Json object");
+      }
+
+      Json::Value::Members members = warnings.getMemberNames();
+
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const std::string& name = members[i];
+        bool enabled = warnings[name].asBool();
+
+        Warnings warning = Warnings_None;
+        if (name == "W001_TagsBeingReadFromStorage")
+        {
+          warning = Warnings_001_TagsBeingReadFromStorage;
+        }
+        else if (name == "W002_InconsistentDicomTagsInDb")
+        {
+          warning = Warnings_002_InconsistentDicomTagsInDb;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, name + " is not recognized as a valid warning name");
+        }
+
+        if (!enabled)
+        {
+          disabledWarnings_.insert(warning);
+        }
+      }
+    }
+    else
+    {
+      disabledWarnings_.clear();
+    }
+
+  }
+
+
   void OrthancConfiguration::DefaultExtractDicomSummary(DicomMap& target,
                                                         const ParsedDicomFile& dicom)
   {
--- a/OrthancServer/Sources/OrthancConfiguration.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Mon Apr 25 15:50:57 2022 +0200
@@ -27,10 +27,12 @@
 #include "../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h"
 
 #include <OrthancServerResources.h>
+#include "ServerEnumerations.h"
 
 #include <boost/filesystem.hpp>
 #include <boost/thread/shared_mutex.hpp>
 #include <boost/thread/lock_types.hpp>
+#include <set>
 
 class DcmDataset;
 
@@ -42,7 +44,8 @@
   class ParsedDicomFile;
   class ServerIndex;
   class TemporaryFile;
-  
+
+
   class OrthancConfiguration : public boost::noncopyable
   {
   private:
@@ -58,6 +61,7 @@
     Modalities               modalities_;
     Peers                    peers_;
     ServerIndex*             serverIndex_;
+    std::set<Warnings>       disabledWarnings_;
 
     OrthancConfiguration() :
       configurationFileArg_(NULL),
@@ -153,7 +157,9 @@
 
     // "SetServerIndex()" must have been called
     void LoadModalitiesAndPeers();
-    
+
+    void LoadWarnings();
+
     void RegisterFont(ServerResources::FileResourceId resource);
 
     bool LookupStringParameter(std::string& target,
@@ -242,6 +248,11 @@
 
     std::string GetDatabaseServerIdentifier() const;
 
+    bool IsWarningEnabled(Warnings warning) const
+    {
+      return disabledWarnings_.count(warning) == 0;
+    }
+
     static void DefaultExtractDicomSummary(DicomMap& target,
                                            const ParsedDicomFile& dicom);
 
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -38,275 +38,28 @@
 
 namespace Orthanc
 {
-  static void GetChildren(std::list<std::string>& target,
-                          ServerIndex& index,
-                          const std::list<std::string>& source)
-  {
-    target.clear();
-
-    for (std::list<std::string>::const_iterator
-           it = source.begin(); it != source.end(); ++it)
-    {
-      std::list<std::string> tmp;
-      index.GetChildren(tmp, *it);
-      target.splice(target.end(), tmp);
-    }
-  }
-
-
-  static void StoreSetOfStrings(DicomMap& result,
-                                const DicomTag& tag,
-                                const std::set<std::string>& values)
-  {
-    bool isFirst = true;
-
-    std::string s;
-    for (std::set<std::string>::const_iterator
-           it = values.begin(); it != values.end(); ++it)
-    {
-      if (isFirst)
-      {
-        isFirst = false;
-      }
-      else
-      {
-        s += "\\";
-      }
-
-      s += *it;
-    }
-
-    result.SetValue(tag, s, false);
-  }
-
-
-  static void ComputePatientCounters(DicomMap& result,
-                                     ServerIndex& index,
-                                     const std::string& patient,
-                                     const DicomMap& query)
-  {
-    std::list<std::string> studies;
-    index.GetChildren(studies, patient);
-
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES,
-                      boost::lexical_cast<std::string>(studies.size()), false);
-    }
-
-    if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) &&
-        !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
-    {
-      return;
-    }
-
-    std::list<std::string> series;
-    GetChildren(series, index, studies);
-    studies.clear();  // This information is useless below
-    
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES,
-                      boost::lexical_cast<std::string>(series.size()), false);
-    }
-
-    if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
-    {
-      return;
-    }
-
-    std::list<std::string> instances;
-    GetChildren(instances, index, series);
-
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES,
-                      boost::lexical_cast<std::string>(instances.size()), false);
-    }
-  }
-
-
-  static void ComputeStudyCounters(DicomMap& result,
-                                   ServerContext& context,
-                                   const std::string& study,
-                                   const DicomMap& query)
-  {
-    ServerIndex& index = context.GetIndex();
-
-    std::list<std::string> series;
-    index.GetChildren(series, study);
-    
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES,
-                      boost::lexical_cast<std::string>(series.size()), false);
-    }
-
-    if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
-    {
-      std::set<std::string> values;
-
-      for (std::list<std::string>::const_iterator
-             it = series.begin(); it != series.end(); ++it)
-      {
-        DicomMap tags;
-        if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series))
-        {
-          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
-
-          if (value != NULL &&
-              !value->IsNull() &&
-              !value->IsBinary())
-          {
-            values.insert(value->GetContent());
-          }
-        }
-      }
-
-      StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values);
-    }
-
-    if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) &&
-        !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
-    {
-      return;
-    }
-
-    std::list<std::string> instances;
-    GetChildren(instances, index, series);
-
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES,
-                      boost::lexical_cast<std::string>(instances.size()), false);
-    }
-
-    if (query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
-    {
-      std::set<std::string> values;
-
-      for (std::list<std::string>::const_iterator
-             it = instances.begin(); it != instances.end(); ++it)
-      {
-        std::string value;
-        if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid))
-        {
-          values.insert(value);
-        }
-      }
-
-      StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
-    }
-  }
-
-
-  static void ComputeSeriesCounters(DicomMap& result,
-                                    ServerIndex& index,
-                                    const std::string& series,
-                                    const DicomMap& query)
-  {
-    std::list<std::string> instances;
-    index.GetChildren(instances, series);
-
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES,
-                      boost::lexical_cast<std::string>(instances.size()), false);
-    }
-  }
-
-
-  static DicomMap* ComputeCounters(ServerContext& context,
-                                   const std::string& instanceId,
-                                   ResourceType level,
-                                   const DicomMap& query)
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) &&
-            !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) &&
-            !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
-        {
-          return NULL;
-        }
-
-        break;
-
-      case ResourceType_Study:
-        if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) &&
-            !query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) &&
-            !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) &&
-            !query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
-        {
-          return NULL;
-        }
-
-        break;
-
-      case ResourceType_Series:
-        if (!query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES))
-        {
-          return NULL;
-        }
-
-        break;
-
-      default:
-        return NULL;
-    }
-
-    std::string parent;
-    if (!context.GetIndex().LookupParent(parent, instanceId, level))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);  // The resource was deleted in between
-    }
-
-    std::unique_ptr<DicomMap> result(new DicomMap);
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        ComputePatientCounters(*result, context.GetIndex(), parent, query);
-        break;
-
-      case ResourceType_Study:
-        ComputeStudyCounters(*result, context, parent, query);
-        break;
-
-      case ResourceType_Series:
-        ComputeSeriesCounters(*result, context.GetIndex(), parent, query);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    return result.release();
-  }
-
-
   static void AddAnswer(DicomFindAnswers& answers,
+                        ServerContext& context,
+                        const std::string& publicId,
+                        const std::string& instanceId,
                         const DicomMap& mainDicomTags,
                         const Json::Value* dicomAsJson,
+                        ResourceType level,
                         const DicomArray& query,
                         const std::list<DicomTag>& sequencesToReturn,
-                        const DicomMap* counters,
                         const std::string& defaultPrivateCreator,
                         const std::map<uint16_t, std::string>& privateCreators,
                         const std::string& retrieveAet)
   {
-    DicomMap match;
+    ExpandedResource resource;
+    std::set<DicomTag> requestedTags;
+    
+    query.GetTags(requestedTags);
+    requestedTags.erase(DICOM_TAG_QUERY_RETRIEVE_LEVEL); // this is not part of the answer
 
-    if (dicomAsJson != NULL)
-    {
-      match.FromDicomAsJson(*dicomAsJson);
-    }
-    else
-    {
-      match.Assign(mainDicomTags);
-    }
-    
+    // reuse ExpandResource to get missing tags and computed tags (ModalitiesInStudy ...).  This code is therefore shared between C-Find, tools/find, list-resources and QIDO-RS
+    context.ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceDbFlags_IncludeMainDicomTags);
+
     DicomMap result;
 
     /**
@@ -330,7 +83,7 @@
       else
       {
         const DicomTag& tag = query.GetElement(i).GetTag();
-        const DicomValue* value = match.TestAndGetValue(tag);
+        const DicomValue* value = resource.tags_.TestAndGetValue(tag);
 
         if (value != NULL &&
             !value->IsNull() &&
@@ -345,15 +98,6 @@
       }
     }
 
-    if (counters != NULL)
-    {
-      DicomArray tmp(*counters);
-      for (size_t i = 0; i < tmp.GetSize(); i++)
-      {
-        result.SetValue(tmp.GetElement(i).GetTag(), tmp.GetElement(i).GetValue().GetContent(), false);
-      }
-    }
-
     if (result.GetSize() == 0 &&
         sequencesToReturn.empty())
     {
@@ -563,10 +307,8 @@
                        const DicomMap& mainDicomTags,
                        const Json::Value* dicomAsJson) ORTHANC_OVERRIDE
     {
-      std::unique_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_));
-
-      AddAnswer(answers_, mainDicomTags, dicomAsJson, queryAsArray_, sequencesToReturn_,
-                counters.get(), defaultPrivateCreator_, privateCreators_, retrieveAet_);
+      AddAnswer(answers_, context_, publicId, instanceId, mainDicomTags, dicomAsJson, level_, queryAsArray_, sequencesToReturn_,
+                defaultPrivateCreator_, privateCreators_, retrieveAet_);
     }
   };
 
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -200,6 +200,80 @@
     }
   }
 
+  static void LoadMainDicomTags(const Json::Value& configuration)
+  {
+    static const char* const EXTRA_MAIN_DICOM_TAGS = "ExtraMainDicomTags";
+    
+    if (configuration.type() != Json::objectValue ||
+        !configuration.isMember(EXTRA_MAIN_DICOM_TAGS) ||
+        configuration[EXTRA_MAIN_DICOM_TAGS].type() != Json::objectValue)
+    {
+      return;
+    }
+
+    Json::Value::Members levels(configuration[EXTRA_MAIN_DICOM_TAGS].getMemberNames());
+
+    for (Json::Value::ArrayIndex i = 0; i < levels.size(); i++)
+    {
+      ResourceType level;
+      if (levels[i] == "Patient")
+      {
+        level = ResourceType_Patient;
+      }
+      else if (levels[i] == "Study")
+      {
+        level = ResourceType_Study;
+      }
+      else if (levels[i] == "Series")
+      {
+        level = ResourceType_Series;
+      }
+      else if (levels[i] == "Instance")
+      {
+        level = ResourceType_Instance;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Unknown entry '" + levels[i] + "' in ExtraMainDicomTags.");
+      }
+
+      const Json::Value& content = configuration[EXTRA_MAIN_DICOM_TAGS][levels[i]];
+
+      if (content.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "The definition of the '" + levels[i] + "' ExtraMainDicomTags entry is invalid (not an array).");
+      }
+
+      if (content.size() > 0)
+      {
+        LOG(INFO) << "Configured Extra Main Dicom Tags for " << levels[i] << ":";
+
+        for (Json::Value::ArrayIndex t = 0; t < content.size(); t++)
+        {
+          const std::string& tagName = content[t].asString();
+          DicomTag tag(FromDcmtkBridge::ParseTag(tagName));
+
+          if (DicomMap::IsComputedTag(tag))
+          {
+            LOG(WARNING) << "  - " << tagName << " can not be added in the Extra Main Dicom Tags since the value of this tag is computed when requested";
+          }
+          else
+          {
+            ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
+            if (vr == ValueRepresentation_Sequence)
+            {
+              LOG(WARNING) << "  - " << tagName << " can not be added in the Extra Main Dicom Tags since it is a sequence";
+            }
+            else
+            {
+              DicomMap::AddMainDicomTag(tag, tagName, level);
+              LOG(INFO) << "  - " << tagName;
+            }
+          }
+        }
+      }
+    }
+  }
 
   static void ConfigurePkcs11(const Json::Value& config)
   {
@@ -299,6 +373,10 @@
     LoadExternalDictionaries(lock.GetJson());  // New in Orthanc 1.9.4
     LoadCustomDictionary(lock.GetJson());
 
+    lock.GetConfiguration().LoadWarnings();
+
+    LoadMainDicomTags(lock.GetJson());  // New in Orthanc 1.11.0
+
     lock.GetConfiguration().RegisterFont(ServerResources::FONT_UBUNTU_MONO_BOLD_16);
 
 #if HAVE_MALLOPT == 1
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -28,6 +28,7 @@
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/MetricsRegistry.h"
 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../OrthancConfiguration.h"
 #include "../ServerContext.h"
 
@@ -463,10 +464,12 @@
   static const std::string GET_SIMPLIFY = "simplify";
   static const std::string GET_FULL = "full";
   static const std::string GET_SHORT = "short";
+  static const std::string GET_REQUESTED_TAGS = "requestedTags";
 
   static const std::string POST_SIMPLIFY = "Simplify";
   static const std::string POST_FULL = "Full";
   static const std::string POST_SHORT = "Short";
+  static const std::string POST_REQUESTED_TAGS = "RequestedTags";
 
   static const std::string DOCUMENT_SIMPLIFY =
     "report the DICOM tags in human-readable format (using the symbolic name of the tags)";
@@ -525,7 +528,6 @@
     }
   }
 
-
   void OrthancRestApi::DocumentDicomFormat(RestApiGetCall& call,
                                            DicomToJsonFormat defaultFormat)
   {
@@ -570,4 +572,34 @@
                                               "If set to `true`, " + DOCUMENT_FULL, false);
     }
   }
+
+  void OrthancRestApi::GetRequestedTags(std::set<DicomTag>& requestedTags,
+                                        const RestApiGetCall& call)
+  {
+    requestedTags.clear();
+
+    if (call.HasArgument(GET_REQUESTED_TAGS))
+    {
+      try
+      {
+        FromDcmtkBridge::ParseListOfTags(requestedTags, call.GetArgument("requestedTags", ""));
+      }
+      catch (OrthancException& ex)
+      {
+        throw OrthancException(ErrorCode_BadRequest, std::string("Invalid requestedTags argument: ") + ex.What() + " " + ex.GetDetails());
+      }
+    }
+
+  }
+
+  void OrthancRestApi::DocumentRequestedTags(RestApiGetCall& call)
+  {
+      call.GetDocumentation().SetHttpGetArgument(GET_REQUESTED_TAGS, RestApiCallDocumentation::Type_String,
+                          "If present, list the DICOM Tags you want to list in the response.  This argument is a semi-column separated list "
+                          "of DICOM Tags identifiers; e.g: 'requestedTags=0010,0010;PatientBirthDate'.  "
+                          "The tags requested tags are returned in the 'RequestedTags' field in the response.  "
+                          "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response "
+                          "might be slow since Orthanc will need to access the DICOM files.  If not specified, Orthanc will return ", false);
+  }
+
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Mon Apr 25 15:50:57 2022 +0200
@@ -145,5 +145,10 @@
 
     static void DocumentDicomFormat(RestApiPostCall& call,
                                     DicomToJsonFormat defaultFormat);
+
+    static void GetRequestedTags(std::set<DicomTag>& requestedTags,
+                                 const RestApiGetCall& call);
+
+    static void DocumentRequestedTags(RestApiGetCall& call);
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -44,7 +44,7 @@
 
 // This "include" is mandatory for Release builds using Linux Standard Base
 #include <boost/math/special_functions/round.hpp>
-
+#include <boost/shared_ptr.hpp>
 
 /**
  * This semaphore is used to limit the number of concurrent HTTP
@@ -126,11 +126,15 @@
   // List all the patients, studies, series or instances ----------------------
  
   static void AnswerListOfResources(RestApiOutput& output,
-                                    ServerIndex& index,
+                                    ServerContext& context,
                                     const std::list<std::string>& resources,
+                                    const std::map<std::string, std::string>& instancesIds, // optional: the id of an instance for each found resource.
+                                    const std::map<std::string, boost::shared_ptr<DicomMap> >& resourcesMainDicomTags,  // optional: all tags read from DB for a resource (current level and upper levels)
+                                    const std::map<std::string, Json::Value>& resourcesDicomAsJson, // optional: the dicom-as-json for each resource
                                     ResourceType level,
                                     bool expand,
-                                    DicomToJsonFormat format)
+                                    DicomToJsonFormat format,
+                                    const std::set<DicomTag>& requestedTags)
   {
     Json::Value answer = Json::arrayValue;
 
@@ -140,7 +144,7 @@
       if (expand)
       {
         Json::Value expanded;
-        if (index.ExpandResource(expanded, *resource, level, format))
+        if (context.ExpandResource(expanded, *resource, level, format, requestedTags))
         {
           answer.append(expanded);
         }
@@ -155,12 +159,29 @@
   }
 
 
+  static void AnswerListOfResources(RestApiOutput& output,
+                                    ServerContext& context,
+                                    const std::list<std::string>& resources,
+                                    ResourceType level,
+                                    bool expand,
+                                    DicomToJsonFormat format,
+                                    const std::set<DicomTag>& requestedTags)
+  {
+    std::map<std::string, std::string> unusedInstancesIds;
+    std::map<std::string, boost::shared_ptr<DicomMap> > unusedResourcesMainDicomTags;
+    std::map<std::string, Json::Value> unusedResourcesDicomAsJson;
+
+    AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags);
+  }
+
+
   template <enum ResourceType resourceType>
   static void ListResources(RestApiGetCall& call)
   {
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
+      OrthancRestApi::DocumentRequestedTags(call);
 
       const std::string resources = GetResourceTypeText(resourceType, true /* plural */, false /* lower case */);
       call.GetDocumentation()
@@ -178,9 +199,13 @@
     }
     
     ServerIndex& index = OrthancRestApi::GetIndex(call);
+    ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::list<std::string> result;
 
+    std::set<DicomTag> requestedTags;
+    OrthancRestApi::GetRequestedTags(requestedTags, call);
+
     if (call.HasArgument("limit") ||
         call.HasArgument("since"))
     {
@@ -207,8 +232,9 @@
       index.GetAllUuids(result, resourceType);
     }
 
-    AnswerListOfResources(call.GetOutput(), index, result, resourceType, call.HasArgument("expand"),
-                          OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human));
+    AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand"),
+                          OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human),
+                          requestedTags);
   }
 
 
@@ -219,6 +245,7 @@
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
+      OrthancRestApi::DocumentRequestedTags(call);
 
       const std::string resource = GetResourceTypeText(resourceType, false /* plural */, false /* lower case */);
       call.GetDocumentation()
@@ -233,9 +260,12 @@
 
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
 
+    std::set<DicomTag> requestedTags;
+    OrthancRestApi::GetRequestedTags(requestedTags, call);
+
     Json::Value json;
-    if (OrthancRestApi::GetIndex(call).ExpandResource(
-          json, call.GetUriComponent("id", ""), resourceType, format))
+    if (OrthancRestApi::GetContext(call).ExpandResource(
+          json, call.GetUriComponent("id", ""), resourceType, format, requestedTags))
     {
       call.GetOutput().AnswerJson(json);
     }
@@ -2817,6 +2847,12 @@
     private:
       bool                    isComplete_;
       std::list<std::string>  resources_;
+      
+      // cache the data we used during lookup and that we could reuse when building the answers
+      std::map<std::string, std::string> instancesIds_;         // the id of an instance for each found resource.
+      std::map<std::string, boost::shared_ptr<DicomMap> > resourcesMainDicomTags_;  // all tags read from DB for a resource (current level and upper levels)
+      std::map<std::string, Json::Value> resourcesDicomAsJson_; // the dicom-as-json for a resource
+
       DicomToJsonFormat       format_;
 
     public:
@@ -2837,19 +2873,23 @@
       }
 
       virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId   /* unused     */,
-                         const DicomMap& mainDicomTags   /* unused     */,
-                         const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+                         const std::string& instanceId,
+                         const DicomMap& mainDicomTags,
+                         const Json::Value* dicomAsJson)  ORTHANC_OVERRIDE
       {
         resources_.push_back(publicId);
+        instancesIds_[publicId] = instanceId;
+        resourcesMainDicomTags_[publicId].reset(mainDicomTags.Clone());
+        resourcesDicomAsJson_[publicId] = dicomAsJson;
       }
 
       void Answer(RestApiOutput& output,
-                  ServerIndex& index,
+                  ServerContext& context,
                   ResourceType level,
-                  bool expand) const
+                  bool expand,
+                  const std::set<DicomTag>& requestedTags) const
       {
-        AnswerListOfResources(output, index, resources_, level, expand, format_);
+        AnswerListOfResources(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags);
       }
     };
   }
@@ -2862,6 +2902,7 @@
     static const char* const KEY_LEVEL = "Level";
     static const char* const KEY_LIMIT = "Limit";
     static const char* const KEY_QUERY = "Query";
+    static const char* const KEY_REQUESTED_TAGS = "RequestedTags";
     static const char* const KEY_SINCE = "Since";
 
     if (call.IsDocumentation())
@@ -2884,6 +2925,12 @@
                          "Limit the number of reported resources", false)
         .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number,
                          "Show only the resources since the provided index (in conjunction with `Limit`)", false)
+        .SetRequestField(KEY_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings,
+                         "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true).  "
+                         "The tags requested tags are returned in the 'RequestedTags' field in the response.  "
+                         "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response "
+                         "might be slow since Orthanc will need to access the DICOM files.  If not specified, Orthanc will return "
+                         "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false)
         .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
                          "Associative array containing the filter on the values of the DICOM tags", true)
         .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information "
@@ -2930,6 +2977,12 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_SINCE) + "\" should be an integer");
     }
+    else if (request.isMember(KEY_REQUESTED_TAGS) &&
+             request[KEY_REQUESTED_TAGS].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" should be an array");
+    }
     else
     {
       bool expand = false;
@@ -2970,6 +3023,13 @@
         since = static_cast<size_t>(tmp);
       }
 
+      std::set<DicomTag> requestedTags;
+
+      if (request.isMember(KEY_REQUESTED_TAGS))
+      {
+        FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]);
+      }
+
       ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
 
       DatabaseLookup query;
@@ -2997,7 +3057,7 @@
 
       FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human));
       context.Apply(visitor, query, level, since, limit);
-      visitor.Answer(call.GetOutput(), context.GetIndex(), level, expand);
+      visitor.Answer(call.GetOutput(), context, level, expand, requestedTags);
     }
   }
 
@@ -3009,6 +3069,7 @@
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
+      OrthancRestApi::DocumentRequestedTags(call);
 
       const std::string children = GetResourceTypeText(end, true /* plural */, false /* lower case */);
       const std::string resource = GetResourceTypeText(start, false /* plural */, false /* lower case */);
@@ -3025,6 +3086,9 @@
 
     ServerIndex& index = OrthancRestApi::GetIndex(call);
 
+    std::set<DicomTag> requestedTags;
+    OrthancRestApi::GetRequestedTags(requestedTags, call);
+
     std::list<std::string> a, b, c;
     a.push_back(call.GetUriComponent("id", ""));
 
@@ -3054,7 +3118,7 @@
            it = a.begin(); it != a.end(); ++it)
     {
       Json::Value resource;
-      if (OrthancRestApi::GetIndex(call).ExpandResource(resource, *it, end, format))
+      if (OrthancRestApi::GetContext(call).ExpandResource(resource, *it, end, format, requestedTags))
       {
         result.append(resource);
       }
@@ -3132,6 +3196,7 @@
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
+      OrthancRestApi::DocumentRequestedTags(call);
 
       const std::string parent = GetResourceTypeText(end, false /* plural */, false /* lower case */);
       const std::string resource = GetResourceTypeText(start, false /* plural */, false /* lower case */);
@@ -3147,7 +3212,10 @@
     }
 
     ServerIndex& index = OrthancRestApi::GetIndex(call);
-    
+
+    std::set<DicomTag> requestedTags;
+    OrthancRestApi::GetRequestedTags(requestedTags, call);
+
     std::string current = call.GetUriComponent("id", "");
     ResourceType currentType = start;
     while (currentType > end)
@@ -3169,7 +3237,7 @@
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
 
     Json::Value resource;
-    if (OrthancRestApi::GetIndex(call).ExpandResource(resource, current, end, format))
+    if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags))
     {
       call.GetOutput().AnswerJson(resource);
     }
@@ -3408,7 +3476,7 @@
   {
     static const char* const LEVEL = "Level";
     static const char* const METADATA = "Metadata";
-      
+
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
@@ -3420,7 +3488,7 @@
                          "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true)
         .SetRequestField(LEVEL, RestApiCallDocumentation::Type_String,
                          "This optional argument specifies the level of interest (can be `Patient`, `Study`, `Series` or "
-                         "`Instance`). Orthanc will loop over the items inside `Resources`, and explorer upward or "
+                         "`Instance`). Orthanc will loop over the items inside `Resources`, and explore upward or "
                          "downward in the DICOM hierarchy in order to find the level of interest.", false)
         .SetRequestField(METADATA, RestApiCallDocumentation::Type_Boolean,
                          "If set to `true` (default value), the metadata associated with the resources will also be retrieved.", false)
@@ -3541,7 +3609,9 @@
                it = interest.begin(); it != interest.end(); ++it)
         {
           Json::Value item;
-          if (index.ExpandResource(item, *it, level, format))
+          std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
+
+          if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags))
           {
             if (metadata)
             {
@@ -3563,8 +3633,10 @@
         {
           ResourceType level;
           Json::Value item;
+          std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
+
           if (index.LookupResourceType(level, *it) &&
-              index.ExpandResource(item, *it, level, format))
+              OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags))
           {
             if (metadata)
             {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -47,7 +47,17 @@
   {
     call.GetOutput().Redirect("app/images/favicon.ico");
   }
- 
+
+  static void GetMainDicomTagsConfiguration(Json::Value& result)
+  {
+      Json::Value v;
+      
+      result["Patient"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Patient);
+      result["Study"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Study);
+      result["Series"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Series);
+      result["Instance"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance);
+  }
+
   static void GetSystemInformation(RestApiGetCall& call)
   {
     static const char* const API_VERSION = "ApiVersion";
@@ -62,6 +72,8 @@
     static const char* const PLUGINS_ENABLED = "PluginsEnabled";
     static const char* const STORAGE_AREA_PLUGIN = "StorageAreaPlugin";
     static const char* const VERSION = "Version";
+    static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
+    static const char* const STORAGE_COMPRESSION = "StorageCompression";
     
     if (call.IsDocumentation())
     {
@@ -88,6 +100,10 @@
                         "Whether Orthanc was built with support for plugins")
         .SetAnswerField(CHECK_REVISIONS, RestApiCallDocumentation::Type_Boolean,
                         "Whether Orthanc handle revisions of metadata and attachments to deal with multiple writers (new in Orthanc 1.9.2)")
+        .SetAnswerField(MAIN_DICOM_TAGS, RestApiCallDocumentation::Type_JsonObject,
+                        "The list of MainDicomTags saved in DB for each resource level (new in Orthanc 1.11.0)")
+        .SetAnswerField(STORAGE_COMPRESSION, RestApiCallDocumentation::Type_Boolean,
+                        "Whether storage compression is enabled (new in Orthanc 1.11.0)")
         .SetHttpGetSample("https://demo.orthanc-server.com/system", true);
       return;
     }
@@ -108,6 +124,7 @@
       result[HTTP_PORT] = lock.GetConfiguration().GetUnsignedIntegerParameter(HTTP_PORT, 8042);
       result[NAME] = lock.GetConfiguration().GetStringParameter(NAME, "");
       result[CHECK_REVISIONS] = lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false);  // New in Orthanc 1.9.2
+      result[STORAGE_COMPRESSION] = lock.GetConfiguration().GetBooleanParameter(STORAGE_COMPRESSION, false); // New in Orthanc 1.11.0
     }
 
     result[STORAGE_AREA_PLUGIN] = Json::nullValue;
@@ -132,6 +149,9 @@
     result[PLUGINS_ENABLED] = false;
 #endif
 
+    result[MAIN_DICOM_TAGS] = Json::objectValue;
+    GetMainDicomTagsConfiguration(result[MAIN_DICOM_TAGS]);
+
     call.GetOutput().AnswerJson(result);
   }
 
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -259,7 +259,9 @@
                        const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
     {
       Json::Value resource;
-      if (context_.GetIndex().ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human))
+      std::set<DicomTag> emptyRequestedTags;  // not supported for webdav
+
+      if (context_.ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human, emptyRequestedTags))
       {
         if (success_)
         {
--- a/OrthancServer/Sources/Search/DatabaseLookup.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -281,14 +281,13 @@
 
   bool DatabaseLookup::HasOnlyMainDicomTags() const
   {
-    std::set<DicomTag> mainTags;
-    DicomMap::GetMainDicomTags(mainTags);
+    const std::set<DicomTag>& allMainTags = DicomMap::GetAllMainDicomTags();
 
     for (size_t i = 0; i < constraints_.size(); i++)
     {
       assert(constraints_[i] != NULL);
       
-      if (mainTags.find(constraints_[i]->GetTag()) == mainTags.end())
+      if (allMainTags.find(constraints_[i]->GetTag()) == allMainTags.end())
       {
         // This is not a main DICOM tag
         return false;
@@ -327,4 +326,45 @@
 
     return false;
   }
+
+  bool DatabaseLookup::GetConstraint(const DicomTagConstraint*& constraint, const DicomTag& tag) const
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      if (constraints_[i]->GetTag() == tag)
+      {
+        constraint = constraints_.at(i);
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  void DatabaseLookup::RemoveConstraint(const DicomTag& tag)
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      if (constraints_[i]->GetTag() == tag)
+      {
+        delete constraints_[i];
+        constraints_.erase(constraints_.begin() + i);
+      }
+    }
+  }
+
+  DatabaseLookup* DatabaseLookup::Clone() const
+  {
+    std::unique_ptr<DatabaseLookup> clone(new DatabaseLookup());
+
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      clone->AddConstraint(*(new DicomTagConstraint(*constraints_[i])));
+    }
+
+    return clone.release();
+  }
 }
--- a/OrthancServer/Sources/Search/DatabaseLookup.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/Search/DatabaseLookup.h	Mon Apr 25 15:50:57 2022 +0200
@@ -48,6 +48,8 @@
 
     ~DatabaseLookup();
 
+    DatabaseLookup* Clone() const;
+
     void Reserve(size_t n)
     {
       constraints_.reserve(n);
@@ -60,6 +62,8 @@
 
     const DicomTagConstraint& GetConstraint(size_t index) const;
 
+    bool GetConstraint(const DicomTagConstraint*& constraint, const DicomTag& tag) const;
+
     bool IsMatch(const DicomMap& value) const;
 
     bool IsMatch(DcmItem& item,
@@ -86,5 +90,7 @@
     std::string Format() const;
 
     bool HasTag(const DicomTag& tag) const;
+
+    void RemoveConstraint(const DicomTag& tag);
   };
 }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -214,7 +214,7 @@
   }
 
 
-  bool DicomTagConstraint::IsMatch(const std::string& value)
+  bool DicomTagConstraint::IsMatch(const std::string& value) const
   {
     NormalizedString source(value, caseSensitive_);
 
@@ -269,7 +269,7 @@
   }
 
 
-  bool DicomTagConstraint::IsMatch(const DicomMap& value)
+  bool DicomTagConstraint::IsMatch(const DicomMap& value) const
   {
     const DicomValue* tmp = value.TestAndGetValue(tag_);
 
--- a/OrthancServer/Sources/Search/DicomTagConstraint.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.h	Mon Apr 25 15:50:57 2022 +0200
@@ -42,7 +42,7 @@
     bool                    caseSensitive_;
     bool                    mandatory_;
 
-    boost::shared_ptr<RegularExpression>  regex_;
+    mutable boost::shared_ptr<RegularExpression>  regex_;  // mutable because the regex is an internal object created only when required (in IsMatch const method)
 
     void AssignSingleValue(const std::string& value);
 
@@ -102,9 +102,9 @@
       return values_;
     }
 
-    bool IsMatch(const std::string& value);
+    bool IsMatch(const std::string& value) const;
 
-    bool IsMatch(const DicomMap& value);
+    bool IsMatch(const DicomMap& value) const;
 
     std::string Format() const;
 
--- a/OrthancServer/Sources/ServerContext.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -64,6 +64,12 @@
 
 namespace Orthanc
 {
+  static void ComputeStudyTags(ExpandedResource& resource,
+                               ServerContext& context,
+                               const std::string& studyPublicId,
+                               const std::set<DicomTag>& requestedTags);
+
+
   static bool IsUncompressedTransferSyntax(DicomTransferSyntax transferSyntax)
   {
     return (transferSyntax == DicomTransferSyntax_LittleEndianImplicit ||
@@ -1354,20 +1360,33 @@
   }
 
 
-  void ServerContext::ApplyInternal(ILookupVisitor& visitor,
-                                    const DatabaseLookup& lookup,
-                                    ResourceType queryLevel,
-                                    size_t since,
-                                    size_t limit)
+  void ServerContext::Apply(ILookupVisitor& visitor,
+                            const DatabaseLookup& lookup,
+                            ResourceType queryLevel,
+                            size_t since,
+                            size_t limit)
   {    
     unsigned int databaseLimit = (queryLevel == ResourceType_Instance ?
                                   limitFindInstances_ : limitFindResults_);
       
     std::vector<std::string> resources, instances;
+    const DicomTagConstraint* dicomModalitiesConstraint = NULL;
+
+    bool hasModalitiesInStudyLookup = (queryLevel == ResourceType_Study &&
+          lookup.GetConstraint(dicomModalitiesConstraint, DICOM_TAG_MODALITIES_IN_STUDY) &&
+          ((dicomModalitiesConstraint->GetConstraintType() == ConstraintType_Equal && !dicomModalitiesConstraint->GetValue().empty()) ||
+          (dicomModalitiesConstraint->GetConstraintType() == ConstraintType_List && !dicomModalitiesConstraint->GetValues().empty())));
+
+    std::unique_ptr<DatabaseLookup> fastLookup(lookup.Clone());
+    
+    if (hasModalitiesInStudyLookup)
+    {
+      fastLookup->RemoveConstraint(DICOM_TAG_MODALITIES_IN_STUDY);
+    }
 
     {
       const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);      
-      GetIndex().ApplyLookupResources(resources, &instances, lookup, queryLevel, lookupLimit);
+      GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, lookupLimit);
     }
 
     bool complete = (databaseLimit == 0 ||
@@ -1396,16 +1415,16 @@
 
       bool hasOnlyMainDicomTags;
       DicomMap dicom;
+      DicomMap allMainDicomTagsFromDB;
       
       if (findStorageAccessMode_ == FindStorageAccessMode_DatabaseOnly ||
           findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer ||
-          lookup.HasOnlyMainDicomTags())
+          fastLookup->HasOnlyMainDicomTags())
       {
         // Case (1): The main DICOM tags, as stored in the database,
         // are sufficient to look for match
 
-        DicomMap tmp;
-        if (!GetIndex().GetAllMainDicomTags(tmp, instances[i]))
+        if (!GetIndex().GetAllMainDicomTags(allMainDicomTagsFromDB, instances[i]))
         {
           // The instance has been removed during the execution of the
           // lookup, ignore it
@@ -1418,16 +1437,16 @@
         {
           // WARNING: Don't reorder cases below, and don't add "break"
           case ResourceType_Instance:
-            dicom.MergeMainDicomTags(tmp, ResourceType_Instance);
+            dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Instance);
 
           case ResourceType_Series:
-            dicom.MergeMainDicomTags(tmp, ResourceType_Series);
+            dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Series);
 
           case ResourceType_Study:
-            dicom.MergeMainDicomTags(tmp, ResourceType_Study);
+            dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Study);
             
           case ResourceType_Patient:
-            dicom.MergeMainDicomTags(tmp, ResourceType_Patient);
+            dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Patient);
             break;
 
           default:
@@ -1449,46 +1468,71 @@
         hasOnlyMainDicomTags = false;   
       }
       
-      if (lookup.IsMatch(dicom))
+      if (fastLookup->IsMatch(dicom))
       {
-        if (skipped < since)
-        {
-          skipped++;
-        }
-        else if (limit != 0 &&
-                 countResults >= limit)
+        bool isMatch = true;
+
+        if (hasModalitiesInStudyLookup)
         {
-          // Too many results, don't mark as complete
-          complete = false;
-          break;
-        }
-        else
-        {
-          if ((findStorageAccessMode_ == FindStorageAccessMode_DiskOnLookupAndAnswer ||
-               findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer) &&
-              dicomAsJson.get() == NULL &&
-              isDicomAsJsonNeeded)
+          std::set<DicomTag> requestedTags;
+          requestedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+          ExpandedResource resource;
+          ComputeStudyTags(resource, *this, resources[i], requestedTags);
+
+          std::vector<std::string> modalities;
+          Toolbox::TokenizeString(modalities, resource.tags_.GetValue(DICOM_TAG_MODALITIES_IN_STUDY).GetContent(), '\\');
+          bool hasAtLeastOneModalityMatching = false;
+          for (size_t m = 0; m < modalities.size(); m++)
           {
-            dicomAsJson.reset(new Json::Value);
-            ReadDicomAsJson(*dicomAsJson, instances[i]);
+            hasAtLeastOneModalityMatching |= dicomModalitiesConstraint->IsMatch(modalities[m]);
           }
 
-          if (hasOnlyMainDicomTags)
+          isMatch = isMatch && hasAtLeastOneModalityMatching;
+          // copy the value of ModalitiesInStudy such that it can be reused to build the answer
+          allMainDicomTagsFromDB.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, resource.tags_.GetValue(DICOM_TAG_MODALITIES_IN_STUDY));
+        }
+
+        if (isMatch)
+        {
+          if (skipped < since)
           {
-            // This is Case (1): The variable "dicom" only contains the main DICOM tags
-            visitor.Visit(resources[i], instances[i], dicom, dicomAsJson.get());
+            skipped++;
+          }
+          else if (limit != 0 &&
+                  countResults >= limit)
+          {
+            // Too many results, don't mark as complete
+            complete = false;
+            break;
           }
           else
           {
-            // Remove the non-main DICOM tags from "dicom" if Case (2)
-            // was used, for consistency with Case (1)
+            if ((findStorageAccessMode_ == FindStorageAccessMode_DiskOnLookupAndAnswer ||
+                findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer) &&
+                dicomAsJson.get() == NULL &&
+                isDicomAsJsonNeeded)
+            {
+              dicomAsJson.reset(new Json::Value);
+              ReadDicomAsJson(*dicomAsJson, instances[i]);
+            }
 
-            DicomMap mainDicomTags;
-            mainDicomTags.ExtractMainDicomTags(dicom);
-            visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get());            
+            if (hasOnlyMainDicomTags)
+            {
+              // This is Case (1): The variable "dicom" only contains the main DICOM tags
+              visitor.Visit(resources[i], instances[i], allMainDicomTagsFromDB, dicomAsJson.get());
+            }
+            else
+            {
+              // Remove the non-main DICOM tags from "dicom" if Case (2)
+              // was used, for consistency with Case (1)
+
+              DicomMap mainDicomTags;
+              mainDicomTags.ExtractMainDicomTags(dicom);
+              visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get());            
+            }
+              
+            countResults ++;
           }
-            
-          countResults ++;
         }
       }
     }
@@ -1501,228 +1545,6 @@
     LOG(INFO) << "Number of matching resources: " << countResults;
   }
 
-
-
-  namespace
-  {
-    class ModalitiesInStudyVisitor : public ServerContext::ILookupVisitor
-    {
-    private:
-      class Study : public boost::noncopyable
-      {
-      private:
-        std::string            orthancId_;
-        std::string            instanceId_;
-        DicomMap               mainDicomTags_;
-        Json::Value            dicomAsJson_;
-        std::set<std::string>  modalitiesInStudy_;
-
-      public:
-        Study(const std::string& instanceId,
-              const DicomMap& seriesTags) :
-          instanceId_(instanceId),
-          dicomAsJson_(Json::nullValue)
-        {
-          {
-            DicomMap tmp;
-            tmp.Assign(seriesTags);
-            tmp.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "dummy", false);
-            DicomInstanceHasher hasher(tmp);
-            orthancId_ = hasher.HashStudy();
-          }
-          
-          mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Study);
-          mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Patient);
-          AddModality(seriesTags);
-        }
-
-        void AddModality(const DicomMap& seriesTags)
-        {
-          std::string modality;
-          if (seriesTags.LookupStringValue(modality, DICOM_TAG_MODALITY, false) &&
-              !modality.empty())
-          {
-            modalitiesInStudy_.insert(modality);
-          }
-        }
-
-        void SetDicomAsJson(const Json::Value& dicomAsJson)
-        {
-          dicomAsJson_ = dicomAsJson;
-        }
-
-        const std::string& GetOrthancId() const
-        {
-          return orthancId_;
-        }
-
-        const std::string& GetInstanceId() const
-        {
-          return instanceId_;
-        }
-
-        const DicomMap& GetMainDicomTags() const
-        {
-          return mainDicomTags_;
-        }
-
-        const Json::Value* GetDicomAsJson() const
-        {
-          if (dicomAsJson_.type() == Json::nullValue)
-          {
-            return NULL;
-          }
-          else
-          {
-            return &dicomAsJson_;
-          }
-        } 
-      };
-      
-      typedef std::map<std::string, Study*>  Studies;
-      
-      bool     isDicomAsJsonNeeded_;
-      bool     complete_;
-      Studies  studies_;
-      
-    public:
-      explicit ModalitiesInStudyVisitor(bool isDicomAsJsonNeeded) :
-        isDicomAsJsonNeeded_(isDicomAsJsonNeeded),
-        complete_(false)
-      {
-      }
-
-      ~ModalitiesInStudyVisitor()
-      {
-        for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it)
-        {
-          assert(it->second != NULL);
-          delete it->second;
-        }
-
-        studies_.clear();
-      }
-      
-      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-      {
-        return isDicomAsJsonNeeded_;
-      }
-      
-      virtual void MarkAsComplete() ORTHANC_OVERRIDE
-      {
-        complete_ = true;
-      }
-      
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId,
-                         const DicomMap& seriesTags,
-                         const Json::Value* dicomAsJson) ORTHANC_OVERRIDE
-      {
-        std::string studyInstanceUid;
-        if (seriesTags.LookupStringValue(studyInstanceUid, DICOM_TAG_STUDY_INSTANCE_UID, false))
-        {
-          Studies::iterator found = studies_.find(studyInstanceUid);
-          if (found == studies_.end())
-          {
-            // New study
-            std::unique_ptr<Study> study(new Study(instanceId, seriesTags));
-            
-            if (dicomAsJson != NULL)
-            {
-              study->SetDicomAsJson(*dicomAsJson);
-            }
-            
-            studies_[studyInstanceUid] = study.release();
-          }
-          else
-          {
-            // Already existing study
-            found->second->AddModality(seriesTags);
-          }
-        }
-      }
-
-      void Forward(ILookupVisitor& callerVisitor,
-                   size_t since,
-                   size_t limit) const
-      {
-        size_t index = 0;
-        size_t countForwarded = 0;
-        
-        for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it, index++)
-        {
-          if (limit == 0 ||
-              (index >= since &&
-               index < limit))
-          {
-            assert(it->second != NULL);
-            const Study& study = *it->second;
-
-            countForwarded++;
-            callerVisitor.Visit(study.GetOrthancId(), study.GetInstanceId(),
-                                study.GetMainDicomTags(), study.GetDicomAsJson());
-          }
-        }
-
-        if (countForwarded == studies_.size())
-        {
-          callerVisitor.MarkAsComplete();
-        }
-      }
-    };
-  }
-  
-
-  void ServerContext::Apply(ILookupVisitor& visitor,
-                            const DatabaseLookup& lookup,
-                            ResourceType queryLevel,
-                            size_t since,
-                            size_t limit)
-  {
-    if (queryLevel == ResourceType_Study &&
-        lookup.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
-    {
-      // Convert the study-level query, into a series-level query,
-      // where "ModalitiesInStudy" is replaced by "Modality"
-      DatabaseLookup seriesLookup;
-
-      for (size_t i = 0; i < lookup.GetConstraintsCount(); i++)
-      {
-        const DicomTagConstraint& constraint = lookup.GetConstraint(i);
-        if (constraint.GetTag() == DICOM_TAG_MODALITIES_IN_STUDY)
-        {
-          if ((constraint.GetConstraintType() == ConstraintType_Equal && constraint.GetValue().empty()) ||
-              (constraint.GetConstraintType() == ConstraintType_List && constraint.GetValues().empty()))
-          {
-            // Ignore universal lookup on "ModalitiesInStudy" (0008,0061),
-            // this should have been handled by the caller
-            ApplyInternal(visitor, lookup, queryLevel, since, limit);
-            return;
-          }
-          else
-          {
-            DicomTagConstraint modality(constraint);
-            modality.SetTag(DICOM_TAG_MODALITY);
-            seriesLookup.AddConstraint(modality);
-          }
-        }
-        else
-        {
-          seriesLookup.AddConstraint(constraint);
-        }
-      }
-
-      ModalitiesInStudyVisitor seriesVisitor(visitor.IsDicomAsJsonNeeded());
-      ApplyInternal(seriesVisitor, seriesLookup, ResourceType_Series, 0, 0);
-      seriesVisitor.Forward(visitor, since, limit);
-    }
-    else
-    {
-      ApplyInternal(visitor, lookup, queryLevel, since, limit);
-    }
-  }
-  
-
   bool ServerContext::LookupOrReconstructMetadata(std::string& target,
                                                   const std::string& publicId,
                                                   ResourceType level,
@@ -2092,4 +1914,543 @@
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
     isUnknownSopClassAccepted_ = accepted;
   }
+
+
+  static void SerializeExpandedResource(Json::Value& target,
+                                        const ExpandedResource& resource,
+                                        DicomToJsonFormat format,
+                                        const std::set<DicomTag>& requestedTags)
+  {
+    target = Json::objectValue;
+
+    target["Type"] = GetResourceTypeText(resource.type_, false, true);
+    target["ID"] = resource.id_;
+
+    switch (resource.type_)
+    {
+      case ResourceType_Patient:
+        break;
+
+      case ResourceType_Study:
+        target["ParentPatient"] = resource.parentId_;
+        break;
+
+      case ResourceType_Series:
+        target["ParentStudy"] = resource.parentId_;
+        break;
+
+      case ResourceType_Instance:
+        target["ParentSeries"] = resource.parentId_;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    switch (resource.type_)
+    {
+      case ResourceType_Patient:
+      case ResourceType_Study:
+      case ResourceType_Series:
+      {
+        Json::Value c = Json::arrayValue;
+
+        for (std::list<std::string>::const_iterator
+                it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it)
+        {
+          c.append(*it);
+        }
+
+        if (resource.type_ == ResourceType_Patient)
+        {
+          target["Studies"] = c;
+        }
+        else if (resource.type_ == ResourceType_Study)
+        {
+          target["Series"] = c;
+        }
+        else
+        {
+          target["Instances"] = c;
+        }
+        break;
+      }
+
+      case ResourceType_Instance:
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    switch (resource.type_)
+    {
+      case ResourceType_Patient:
+      case ResourceType_Study:
+        break;
+
+      case ResourceType_Series:
+        if (resource.expectedNumberOfInstances_ < 0)
+        {
+          target["ExpectedNumberOfInstances"] = Json::nullValue;
+        }
+        else
+        {
+          target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_;
+        }
+        target["Status"] = resource.status_;
+        break;
+
+      case ResourceType_Instance:
+      {
+        target["FileSize"] = static_cast<unsigned int>(resource.fileSize_);
+        target["FileUuid"] = resource.fileUuid_;
+
+        if (resource.indexInSeries_ < 0)
+        {
+          target["IndexInSeries"] = Json::nullValue;
+        }
+        else
+        {
+          target["IndexInSeries"] = resource.indexInSeries_;
+        }
+
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (!resource.anonymizedFrom_.empty())
+    {
+      target["AnonymizedFrom"] = resource.anonymizedFrom_;
+    }
+    
+    if (!resource.modifiedFrom_.empty())
+    {
+      target["ModifiedFrom"] = resource.modifiedFrom_;
+    }
+
+    if (resource.type_ == ResourceType_Patient ||
+        resource.type_ == ResourceType_Study ||
+        resource.type_ == ResourceType_Series)
+    {
+      target["IsStable"] = resource.isStable_;
+
+      if (!resource.lastUpdate_.empty())
+      {
+        target["LastUpdate"] = resource.lastUpdate_;
+      }
+    }
+
+    // serialize tags
+
+    static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
+    static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
+
+    DicomMap mainDicomTags;
+    resource.tags_.ExtractResourceInformation(mainDicomTags, resource.type_);
+
+    target[MAIN_DICOM_TAGS] = Json::objectValue;
+    FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format);
+    
+    if (resource.type_ == ResourceType_Study)
+    {
+      DicomMap patientMainDicomTags;
+      resource.tags_.ExtractPatientInformation(patientMainDicomTags);
+
+      target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
+    }
+
+    if (requestedTags.size() > 0)
+    {
+      static const char* const REQUESTED_TAGS = "RequestedTags";
+
+      DicomMap tags;
+      resource.tags_.ExtractTags(tags, requestedTags);
+
+      target[REQUESTED_TAGS] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format);
+    }
+
+  }
+
+
+  static void ComputeInstanceTags(ExpandedResource& resource,
+                                  ServerContext& context,
+                                  const std::string& instancePublicId,
+                                  const std::set<DicomTag>& requestedTags)
+  {
+    if (requestedTags.count(DICOM_TAG_INSTANCE_AVAILABILITY) > 0)
+    {
+      resource.tags_.SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_INSTANCE_AVAILABILITY);
+    }
+  }
+
+
+  static void ComputeSeriesTags(ExpandedResource& resource,
+                                ServerContext& context,
+                                const std::string& seriesPublicId,
+                                const std::set<DicomTag>& requestedTags)
+  {
+    if (requestedTags.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0)
+    {
+      ServerIndex& index = context.GetIndex();
+      std::list<std::string> instances;
+
+      index.GetChildren(instances, seriesPublicId);
+
+      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES,
+                              boost::lexical_cast<std::string>(instances.size()), false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
+    }
+  }
+
+  static void ComputeStudyTags(ExpandedResource& resource,
+                               ServerContext& context,
+                               const std::string& studyPublicId,
+                               const std::set<DicomTag>& requestedTags)
+  {
+    ServerIndex& index = context.GetIndex();
+    std::list<std::string> series;
+    std::list<std::string> instances;
+
+    bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0;
+    bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0;
+    bool hasModalitiesInStudy = requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0;
+    bool hasSopClassesInStudy = requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0;
+
+    index.GetChildren(series, studyPublicId);
+
+    if (hasModalitiesInStudy)
+    {
+      std::set<std::string> values;
+
+      for (std::list<std::string>::const_iterator
+            it = series.begin(); it != series.end(); ++it)
+      {
+        DicomMap tags;
+        index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series);
+
+        const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
+
+        if (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
+        {
+          values.insert(value->GetContent());
+        }
+      }
+
+      std::string modalities;
+      Toolbox::JoinStrings(modalities, values, "\\");
+
+      resource.tags_.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_MODALITIES_IN_STUDY);
+    }
+
+    if (hasNbRelatedSeries)
+    {
+      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES,
+                              boost::lexical_cast<std::string>(series.size()), false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
+    }
+
+    if (hasNbRelatedInstances || hasSopClassesInStudy)
+    {
+      for (std::list<std::string>::const_iterator
+            it = series.begin(); it != series.end(); ++it)
+      {
+        std::list<std::string> seriesInstancesIds;
+        index.GetChildren(seriesInstancesIds, *it);
+
+        instances.splice(instances.end(), seriesInstancesIds);
+      }
+
+      if (hasNbRelatedInstances)
+      {
+        resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES,
+                                boost::lexical_cast<std::string>(instances.size()), false);      
+        resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
+      }
+
+      if (hasSopClassesInStudy)
+      {
+        std::set<std::string> values;
+
+        for (std::list<std::string>::const_iterator
+              it = instances.begin(); it != instances.end(); ++it)
+        {
+          std::string value;
+
+          if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid))
+          {
+            values.insert(value);
+          }
+        }
+
+        if (values.size() > 0)
+        {
+          std::string sopClassUids;
+          Toolbox::JoinStrings(sopClassUids, values, "\\");
+          resource.tags_.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false);
+        }
+
+        resource.missingRequestedTags_.erase(DICOM_TAG_SOP_CLASSES_IN_STUDY);
+      }
+    }
+  }
+
+  static void ComputePatientTags(ExpandedResource& resource,
+                                 ServerContext& context,
+                                 const std::string& patientPublicId,
+                                 const std::set<DicomTag>& requestedTags)
+  {
+    ServerIndex& index = context.GetIndex();
+
+    std::list<std::string> studies;
+    std::list<std::string> series;
+    std::list<std::string> instances;
+
+    bool hasNbRelatedStudies = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) > 0;
+    bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) > 0;
+    bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) > 0;
+
+    index.GetChildren(studies, patientPublicId);
+
+    if (hasNbRelatedStudies)
+    {
+      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES,
+                              boost::lexical_cast<std::string>(studies.size()), false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
+    }
+
+    if (hasNbRelatedSeries || hasNbRelatedInstances)
+    {
+      for (std::list<std::string>::const_iterator
+            it = studies.begin(); it != studies.end(); ++it)
+      {
+        std::list<std::string> thisSeriesIds;
+        index.GetChildren(thisSeriesIds, *it);
+        series.splice(series.end(), thisSeriesIds);
+      }
+
+      if (hasNbRelatedSeries)
+      {
+        resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES,
+                                boost::lexical_cast<std::string>(series.size()), false);
+        resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
+      }
+    }
+
+    if (hasNbRelatedInstances)
+    {
+      for (std::list<std::string>::const_iterator
+            it = series.begin(); it != series.end(); ++it)
+      {
+        std::list<std::string> thisInstancesIds;
+        index.GetChildren(thisInstancesIds, *it);
+        instances.splice(instances.end(), thisInstancesIds);
+      }
+
+      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES,
+                              boost::lexical_cast<std::string>(instances.size()), false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
+    }
+  }
+
+
+  static void ComputeTags(ExpandedResource& resource,
+                          ServerContext& context,
+                          const std::string& resourceId,
+                          ResourceType level,
+                          const std::set<DicomTag>& requestedTags)
+  {
+    if (level == ResourceType_Patient 
+        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Patient))
+    {
+      ComputePatientTags(resource, context, resourceId, requestedTags);
+    }
+
+    if (level == ResourceType_Study 
+        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Study))
+    {
+      ComputeStudyTags(resource, context, resourceId, requestedTags);
+    }
+
+    if (level == ResourceType_Series 
+        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Series))
+    {
+      ComputeSeriesTags(resource, context, resourceId, requestedTags);
+    }
+
+    if (level == ResourceType_Instance 
+        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Instance))
+    {
+      ComputeInstanceTags(resource, context, resourceId, requestedTags);
+    }
+  }
+
+  bool ServerContext::ExpandResource(Json::Value& target,
+                                     const std::string& publicId,
+                                     ResourceType level,
+                                     DicomToJsonFormat format,
+                                     const std::set<DicomTag>& requestedTags)
+  {
+    std::string unusedInstanceId;
+    Json::Value* unusedDicomAsJson = NULL;
+    DicomMap unusedMainDicomTags;
+
+    return ExpandResource(target, publicId, unusedMainDicomTags, unusedInstanceId, unusedDicomAsJson, level, format, requestedTags);
+  }
+
+  bool ServerContext::ExpandResource(Json::Value& target,
+                                     const std::string& publicId,
+                                     const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
+                                     const std::string& instanceId,    // optional: the id of an instance for the resource (if already available)
+                                      const Json::Value* dicomAsJson,  // optional: the dicom-as-json for the resource (if already available)
+                                     ResourceType level,
+                                     DicomToJsonFormat format,
+                                     const std::set<DicomTag>& requestedTags)
+  {
+    ExpandedResource resource;
+
+    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceDbFlags_Default))
+    {
+      SerializeExpandedResource(target, resource, format, requestedTags);
+      return true;
+    }
+
+    return false;
+  }
+  
+  bool ServerContext::ExpandResource(ExpandedResource& resource,
+                                     const std::string& publicId,
+                                     const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
+                                     const std::string& instanceId,    // optional: the id of an instance for the resource (if already available)
+                                     const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource (if already available)
+                                     ResourceType level,
+                                     const std::set<DicomTag>& requestedTags,
+                                     ExpandResourceDbFlags expandFlags)
+  {
+    // first try to get the tags from what is already available
+    
+    if ((expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags)
+      && (mainDicomTags.GetSize() > 0)
+      && (dicomAsJson != NULL))
+    {
+      
+      if (mainDicomTags.GetSize() > 0)
+      {
+        resource.tags_.Merge(mainDicomTags);
+      }
+
+      if (dicomAsJson != NULL && dicomAsJson->isObject())
+      {
+        resource.tags_.FromDicomAsJson(*dicomAsJson);
+      }
+
+      std::set<DicomTag> retrievedTags;
+      std::set<DicomTag> missingTags;
+      resource.tags_.GetTags(retrievedTags);
+
+      Toolbox::GetMissingsFromSet(missingTags, requestedTags, retrievedTags);
+
+      // if all possible tags have been read, no need to get them from DB anymore
+      if (missingTags.size() == 0 || DicomMap::HasOnlyComputedTags(missingTags))
+      {
+        expandFlags = static_cast<ExpandResourceDbFlags>(expandFlags & ~ExpandResourceDbFlags_IncludeMainDicomTags);
+      }
+
+      if (missingTags.size() == 0 && expandFlags == ExpandResourceDbFlags_None)  // we have already retrieved anything we need
+      {
+        return true;
+      }
+    }
+
+    if (expandFlags != ExpandResourceDbFlags_None
+        && GetIndex().ExpandResource(resource, publicId, level, requestedTags, static_cast<ExpandResourceDbFlags>(expandFlags | ExpandResourceDbFlags_IncludeMetadata)))  // we always need the metadata to get the mainDicomTagsSignature
+    {
+      // check the main dicom tags list has not changed since the resource was stored
+      if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.type_))
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb))
+        {
+          LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.type_, false , false) << " has been stored with another version of Main Dicom Tags list, you should POST to /" << Orthanc::GetResourceTypeText(resource.type_, true, false) << "/" << resource.id_ << "/reconstruct to update the list of tags saved in DB.  Some MainDicomTags might be missing from this answer.";
+        }
+      }
+
+      // possibly merge missing requested tags from dicom-as-json
+      if (!resource.missingRequestedTags_.empty() && !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_))
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage))
+        {
+          std::set<DicomTag> missingTags;
+          Toolbox::AppendSets(missingTags, resource.missingRequestedTags_);
+          for (std::set<DicomTag>::const_iterator it = resource.missingRequestedTags_.begin(); it != resource.missingRequestedTags_.end(); it++)
+          {
+            if (DicomMap::IsComputedTag(*it))
+            {
+              missingTags.erase(*it);
+            }
+          }
+
+          std::string missings;
+          FromDcmtkBridge::FormatListOfTags(missings, missingTags);
+
+          LOG(WARNING) << "W001: Accessing Dicom tags from storage when accessing " << Orthanc::GetResourceTypeText(resource.type_, false , false) << " : " << missings;
+        }
+
+
+        std::string instanceId_ = instanceId;
+        DicomMap tagsFromJson;
+
+        if (dicomAsJson == NULL)
+        {
+          if (instanceId_.empty())
+          {
+            if (level == ResourceType_Instance)
+            {
+              instanceId_ = publicId;
+            }
+            else
+            {
+              std::list<std::string> instancesIds;
+              GetIndex().GetChildInstances(instancesIds, publicId);
+              if (instancesIds.size() < 1)
+              {
+                throw OrthancException(ErrorCode_InternalError, "ExpandResource: no instances found");
+              }
+              instanceId_ = instancesIds.front();
+            }
+          }
+  
+          Json::Value tmpDicomAsJson;
+          ReadDicomAsJson(tmpDicomAsJson, instanceId_);
+          tagsFromJson.FromDicomAsJson(tmpDicomAsJson);
+        }
+        else
+        {
+          tagsFromJson.FromDicomAsJson(*dicomAsJson);
+        }
+
+        resource.tags_.Merge(tagsFromJson);
+      }
+
+      // compute the requested tags
+      ComputeTags(resource, *this, publicId, level, requestedTags);
+    }
+    else
+    {
+      return false;
+    }
+
+    return true;
+  }
+
 }
--- a/OrthancServer/Sources/ServerContext.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Mon Apr 25 15:50:57 2022 +0200
@@ -263,12 +263,6 @@
                                       DicomInstanceToStore& dicom,
                                       StoreInstanceMode mode);
 
-    void ApplyInternal(ILookupVisitor& visitor,
-                       const DatabaseLookup& lookup,
-                       ResourceType queryLevel,
-                       size_t since,
-                       size_t limit);
-
     void PublishDicomCacheMetrics();
 
     // This method must only be called from "ServerIndex"!
@@ -539,5 +533,30 @@
     bool IsUnknownSopClassAccepted();
 
     void SetUnknownSopClassAccepted(bool accepted);
+
+    bool ExpandResource(Json::Value& target,
+                        const std::string& publicId,
+                        ResourceType level,
+                        DicomToJsonFormat format,
+                        const std::set<DicomTag>& requestedTags);
+
+    bool ExpandResource(Json::Value& target,
+                        const std::string& publicId,
+                        const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
+                        const std::string& instanceId,    // optional: the id of an instance for the resource
+                        const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource
+                        ResourceType level,
+                        DicomToJsonFormat format,
+                        const std::set<DicomTag>& requestedTags);
+
+    bool ExpandResource(ExpandedResource& target,
+                        const std::string& publicId,
+                        const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
+                        const std::string& instanceId,    // optional: the id of an instance for the resource
+                        const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource
+                        ResourceType level,
+                        const std::set<DicomTag>& requestedTags,
+                        ExpandResourceDbFlags expandFlags);
+
   };
 }
--- a/OrthancServer/Sources/ServerEnumerations.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -60,6 +60,7 @@
     dictMetadataType_.Add(MetadataType_Instance_CalledAet, "CalledAET");
     dictMetadataType_.Add(MetadataType_Instance_HttpUsername, "HttpUsername");
     dictMetadataType_.Add(MetadataType_Instance_PixelDataOffset, "PixelDataOffset");
+    dictMetadataType_.Add(MetadataType_MainDicomTagsSignature, "MainDicomTagsSignature");
 
     dictContentType_.Add(FileContentType_Dicom, "dicom");
     dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json");
--- a/OrthancServer/Sources/ServerEnumerations.h	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Mon Apr 25 15:50:57 2022 +0200
@@ -151,7 +151,8 @@
     MetadataType_Instance_CalledAet = 12,        // New in Orthanc 1.4.0
     MetadataType_Instance_HttpUsername = 13,     // New in Orthanc 1.4.0
     MetadataType_Instance_PixelDataOffset = 14,  // New in Orthanc 1.9.0
-
+    MetadataType_MainDicomTagsSignature = 15,    // New in Orthanc 1.11.0
+    
     // Make sure that the value "65535" can be stored into this enumeration
     MetadataType_StartUser = 1024,
     MetadataType_EndUser = 65535
@@ -190,6 +191,12 @@
     BuiltinDecoderTranscoderOrder_Disabled
   };
 
+  enum Warnings
+  {
+    Warnings_None,
+    Warnings_001_TagsBeingReadFromStorage,
+    Warnings_002_InconsistentDicomTagsInDb,
+  };
 
 
   void InitializeServerEnumerations();
--- a/OrthancServer/Sources/ServerToolbox.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -107,29 +107,7 @@
       // example). Take this improvement into consideration for the
       // next upgrade of the database schema.
 
-      const char* plural = NULL;
-
-      switch (level)
-      {
-        case ResourceType_Patient:
-          plural = "patients";
-          break;
-
-        case ResourceType_Study:
-          plural = "studies";
-          break;
-
-        case ResourceType_Series:
-          plural = "series";
-          break;
-
-        case ResourceType_Instance:
-          plural = "instances";
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
+      const char* plural = Orthanc::GetResourceTypeText(level, true, true);
 
       LOG(WARNING) << "Upgrade: Reconstructing the main DICOM tags of all the " << plural << "...";
 
@@ -183,6 +161,9 @@
           ResourcesContent tags(false /* prevent the setting of metadata */);
           tags.AddResource(resource, level, dicomSummary);
           transaction.SetResourcesContent(tags);
+
+          transaction.DeleteMetadata(resource, MetadataType_MainDicomTagsSignature);
+          transaction.SetMetadata(resource, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(level), 0);
         }
         catch (OrthancException&)
         {
--- a/OrthancServer/Sources/main.cpp	Wed Apr 13 10:51:22 2022 +0200
+++ b/OrthancServer/Sources/main.cpp	Mon Apr 25 15:50:57 2022 +0200
@@ -770,7 +770,7 @@
     << std::endl << std::endl;
 
   // The content of the following brackets is automatically generated
-  // by the "GenerateErrorCodes.py" script
+  // by the "Resources/CodeGeneration/GenerateErrorCodes.py" script
   {
     PrintErrorCode(ErrorCode_InternalError, "Internal error");
     PrintErrorCode(ErrorCode_Success, "Success");
@@ -817,6 +817,7 @@
     PrintErrorCode(ErrorCode_BadRange, "Incorrect range request");
     PrintErrorCode(ErrorCode_DatabaseCannotSerialize, "Database could not serialize access due to concurrent update, the transaction should be retried");
     PrintErrorCode(ErrorCode_Revision, "A bad revision number was provided, which might indicate conflict between multiple writers");
+    PrintErrorCode(ErrorCode_MainDicomTagsMultiplyDefined, "A main DICOM Tag has been defined multiple times for the same resource level");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
--- a/TODO	Wed Apr 13 10:51:22 2022 +0200
+++ b/TODO	Mon Apr 25 15:50:57 2022 +0200
@@ -7,7 +7,7 @@
 https://book.orthanc-server.com/contributing.html
 
 
-Some features are being funded by and OpenCollective one-time donations.
+Some features are being funded by an OpenCollective one-time donations.
 selected features are marked with priorities ((1) - higher, (2) - medium, (3) - nice to have)
 
 =======
@@ -122,9 +122,6 @@
   - On SCP side: done by https://hg.orthanc-server.com/orthanc/rev/1ec3e1e18f50
   - On SCU side:
     https://groups.google.com/d/msg/orthanc-users/wPl0g5mqZco/5X1Z8tEzBgAJ
-* Support "Instance Availability" (0008,0056) in C-FIND:
-  http://dicom.nema.org/medical/DICOM/2019a/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3.2
-  https://groups.google.com/d/msg/orthanc-users/hteDgE6igo8/j-ArqD7pBQAJ
 * Check Big Endian transfer syntax in ParsedDicomFile::EmbedImage and
   DicomImageDecoder
 * Strict hierarchical C-FIND:
@@ -158,10 +155,12 @@
   useful in ServerContext::DecodeDicomInstance()
 * (2) DicomMap: create a cache to the main DICOM tags index
 * (3) Check out rapidjson: https://github.com/miloyip/nativejson-benchmark
-* (2) Optimize tools/find with ModalitiesInStudies: 
-  https://groups.google.com/g/orthanc-users/c/aN8nqcRd3jw/m/pmc9ylVeAwAJ.
-  One solution could be: Filter first without ModalitiesInStudies and then
-  cycle through the responses to filter out with ModalitiesInStudies
+* For C-Find results: we could store the computed tags
+    in metadata on some events like NewSeries + DeletedSeries (same for other computer tags).
+    OtherTags that could be saved in Metadata as well:
+    - ModalitiesInStudy
+    - all computed counters at series/study/patient level
+    - RequestAttributesSequence (sequence that must be included in all DicomWeb QIDO-RS for series)
 
 ========
 Database