changeset 4935:acd3f72e2a21 more-tags

split ExpandResource in 2: read from DB and serialize to json. This will allow us to merge requested tags from both the DB and the file system
author Alain Mazy <am@osimis.io>
date Thu, 10 Mar 2022 19:00:43 +0100
parents 94a7b681b340
children 8422e4f99a18
files OrthancFramework/Sources/DicomFormat/DicomMap.cpp OrthancFramework/Sources/DicomFormat/DicomMap.h OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h
diffstat 11 files changed, 374 insertions(+), 153 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Thu Mar 10 19:00:43 2022 +0100
@@ -209,11 +209,9 @@
         tagsIds.insert(it->Format());
       }
 
-      std::string signatureText = boost::algorithm::join(tagsIds, "|");
-      std::string signatureMD5;
-      Toolbox::ComputeMD5(signatureMD5, signatureText);
+      std::string signatureText = boost::algorithm::join(tagsIds, ";");
 
-      return signatureMD5;
+      return signatureText;
     }
 
     void LoadDefaultMainDicomTags(ResourceType level)
@@ -422,29 +420,30 @@
     }
   }
 
+  void DicomMap::ExtractResourceInformation(DicomMap& result, ResourceType level) const
+  {
+    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(level);
+    ExtractTags(result, content_, mainDicomTags);
+  }
 
   void DicomMap::ExtractPatientInformation(DicomMap& result) const
   {
-    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Patient);
-    ExtractTags(result, content_, mainDicomTags);
+    ExtractResourceInformation(result, ResourceType_Patient);
   }
 
   void DicomMap::ExtractStudyInformation(DicomMap& result) const
   {
-    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Study);
-    ExtractTags(result, content_, mainDicomTags);
+    ExtractResourceInformation(result, ResourceType_Study);
   }
 
   void DicomMap::ExtractSeriesInformation(DicomMap& result) const
   {
-    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Series);
-    ExtractTags(result, content_, mainDicomTags);
+    ExtractResourceInformation(result, ResourceType_Series);
   }
 
   void DicomMap::ExtractInstanceInformation(DicomMap& result) const
   {
-    const std::map<DicomTag, std::string>& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Instance);
-    ExtractTags(result, content_, mainDicomTags);
+    ExtractResourceInformation(result, ResourceType_Instance);
   }
 
 
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Thu Mar 10 19:00:43 2022 +0100
@@ -118,6 +118,8 @@
 
     void ExtractInstanceInformation(DicomMap& result) const;
 
+    void ExtractResourceInformation(DicomMap& result, ResourceType level) const;
+
     static void SetupFindPatientTemplate(DicomMap& result);
 
     static void SetupFindStudyTemplate(DicomMap& result);
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Thu Mar 10 19:00:43 2022 +0100
@@ -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,27 @@
     return fields.HasTag(ParseTag(tagName));
   }
 
+
+  // 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);
+      }
+    }
+  }
+
+
   const DicomValue &FromDcmtkBridge::GetValue(const DicomMap &fields,
                                               const std::string &tagName)
   {
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Thu Mar 10 19:00:43 2022 +0100
@@ -175,6 +175,9 @@
 
     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 bool HasTag(const DicomMap& fields,
                        const std::string& tagName);
 
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Thu Mar 10 19:00:43 2022 +0100
@@ -358,6 +358,44 @@
 }
 
 
+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());
+  }
+
+  {// 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());
+  }
+
+}
 
 static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110);
 static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120);
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Mar 10 19:00:43 2022 +0100
@@ -710,46 +710,15 @@
   }
   
 
-  bool StatelessDatabaseOperations::ExpandResource(Json::Value& target,
+  bool StatelessDatabaseOperations::ExpandResource(ExpandedResource& target,
                                                    const std::string& publicId,
                                                    ResourceType level,
                                                    DicomToJsonFormat format)
   {    
     class Operations : public ReadOnlyOperationsT5<
-      bool&, Json::Value&, const std::string&, ResourceType, DicomToJsonFormat>
+      bool&, ExpandedResource&, const std::string&, ResourceType, DicomToJsonFormat>
     {
     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,
@@ -806,9 +775,8 @@
         }
         else
         {
-          Json::Value& target = tuple.get<1>();
-          target = Json::objectValue;
-        
+          ExpandedResource& target = tuple.get<1>();
+
           // Set information about the parent resource (if it exists)
           if (type == ResourceType_Patient)
           {
@@ -824,87 +792,36 @@
               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)
-          {
-            Json::Value c = Json::arrayValue;
-
-            for (std::list<std::string>::const_iterator
-                   it = children.begin(); it != children.end(); ++it)
-            {
-              c.append(*it);
-            }
-
-            switch (type)
-            {
-              case ResourceType_Patient:
-                target["Studies"] = c;
-                break;
-
-              case ResourceType_Study:
-                target["Series"] = c;
-                break;
-
-              case ResourceType_Series:
-                target["Instances"] = c;
-                break;
-
-              default:
-                throw OrthancException(ErrorCode_InternalError);
-            }
-          }
+          transaction.GetChildrenPublicId(target.childrenIds_, internalId);
 
           // Extract the metadata
-          std::map<MetadataType, std::string> metadata;
-          transaction.GetAllMetadata(metadata, internalId);
+          transaction.GetAllMetadata(target.metadata_, internalId);
 
           // Set the resource type
+          target.type_ = type;
+
           switch (type)
           {
             case ResourceType_Patient:
-              target["Type"] = "Patient";
-              break;
-
             case ResourceType_Study:
-              target["Type"] = "Study";
               break;
 
             case ResourceType_Series:
             {
-              target["Type"] = "Series";
-
               int64_t i;
-              if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances))
+              if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Series_ExpectedNumberOfInstances))
               {
-                target["ExpectedNumberOfInstances"] = static_cast<int>(i);
-                target["Status"] = EnumerationToString(transaction.GetSeriesStatus(internalId, i));
+                target.expectedNumberOfInstances_ = static_cast<int>(i);
+                target.status_ = EnumerationToString(transaction.GetSeriesStatus(internalId, i));
               }
               else
               {
-                target["ExpectedNumberOfInstances"] = Json::nullValue;
-                target["Status"] = EnumerationToString(SeriesStatus_Unknown);
+                target.expectedNumberOfInstances_ = -1;
+                target.status_ = EnumerationToString(SeriesStatus_Unknown);
               }
 
               break;
@@ -912,8 +829,6 @@
 
             case ResourceType_Instance:
             {
-              target["Type"] = "Instance";
-
               FileInfo attachment;
               int64_t revision;  // ignored
               if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom))
@@ -921,17 +836,17 @@
                 throw OrthancException(ErrorCode_InternalError);
               }
 
-              target["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize());
-              target["FileUuid"] = attachment.GetUuid();
+              target.fileSize_ = static_cast<unsigned int>(attachment.GetUncompressedSize());
+              target.fileUuid_ = attachment.GetUuid();
 
               int64_t i;
-              if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries))
+              if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Instance_IndexInSeries))
               {
-                target["IndexInSeries"] = static_cast<int>(i);
+                target.indexInSeries_ = static_cast<int>(i);
               }
               else
               {
-                target["IndexInSeries"] = Json::nullValue;
+                target.indexInSeries_ = -1;
               }
 
               break;
@@ -942,46 +857,44 @@
           }
 
           // check the main dicom tags list has not changed since the resource was stored
-          std::string resourceMainDicomTagsSignature = DicomMap::GetDefaultMainDicomTagsSignature(type);
-          LookupStringMetadata(resourceMainDicomTagsSignature, metadata, MetadataType_MainDicomTagsSignature);
-          
-          if (resourceMainDicomTagsSignature != DicomMap::GetMainDicomTagsSignature(type))
-          {
-            OrthancConfiguration::ReaderLock lock;
-            if (lock.GetConfiguration().IsInconsistentDicomTagsLogsEnabled())
-            {
-              LOG(WARNING) << Orthanc::GetResourceTypeText(type, false , false) << " has been stored with another version of Main Dicom Tags list, you should POST to /" << Orthanc::GetResourceTypeText(type, true, false) << "/" << tuple.get<2>() << "/reconstruct to update the list of tags saved in DB.  Some tags might be missing from this answer.";
-            }
-          }
-
+          target.mainDicomTagsSignature_ = DicomMap::GetDefaultMainDicomTagsSignature(type);
+          LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature);
 
           // Record the remaining information
-          target["ID"] = tuple.get<2>();
-          MainDicomTagsToJson(transaction, target, internalId, type, tuple.get<4>());
+          target.id_ = tuple.get<2>();
+
+          // read all tags from DB
+          transaction.GetMainDicomTags(target.tags_, internalId);
+
+          // MORE_TAGS: TODO: eventualy get parent dicom tags if requested ....
 
           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;
         }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Mar 10 19:00:43 2022 +0100
@@ -37,6 +37,33 @@
   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_;
+
+    // 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_;
+  };
+
+
   class StatelessDatabaseOperations : public boost::noncopyable
   {
   public:
@@ -448,7 +475,7 @@
   
     void Apply(IReadWriteOperations& operations);
 
-    bool ExpandResource(Json::Value& target,
+    bool ExpandResource(ExpandedResource& target,
                         const std::string& publicId,
                         ResourceType level,
                         DicomToJsonFormat format);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Mar 10 19:00:43 2022 +0100
@@ -126,7 +126,7 @@
   // List all the patients, studies, series or instances ----------------------
  
   static void AnswerListOfResources(RestApiOutput& output,
-                                    ServerIndex& index,
+                                    ServerContext& context,
                                     const std::list<std::string>& resources,
                                     ResourceType level,
                                     bool expand,
@@ -140,7 +140,7 @@
       if (expand)
       {
         Json::Value expanded;
-        if (index.ExpandResource(expanded, *resource, level, format))
+        if (context.ExpandResource(expanded, *resource, level, format))
         {
           answer.append(expanded);
         }
@@ -178,6 +178,7 @@
     }
     
     ServerIndex& index = OrthancRestApi::GetIndex(call);
+    ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::list<std::string> result;
 
@@ -207,7 +208,7 @@
       index.GetAllUuids(result, resourceType);
     }
 
-    AnswerListOfResources(call.GetOutput(), index, result, resourceType, call.HasArgument("expand"),
+    AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand"),
                           OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human));
   }
 
@@ -226,6 +227,12 @@
         .SetSummary("Get information about some " + resource)
         .SetDescription("Get detailed information about the DICOM " + resource + " whose Orthanc identifier is provided in the URL")
         .SetUriArgument("id", "Orthanc identifier of the " + resource + " of interest")
+        .SetHttpGetArgument("requestedTags", 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)
         .AddAnswerType(MimeType_Json, "Information about the DICOM " + resource)
         .SetHttpGetSample(GetDocumentationSampleResource(resourceType), true);
       return;
@@ -233,9 +240,22 @@
 
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
 
+    std::set<DicomTag> responseTags;
+    if (call.HasArgument("requestedTags"))
+    {
+      try
+      {
+        FromDcmtkBridge::ParseListOfTags(responseTags, call.GetArgument("requestedTags", ""));
+      }
+      catch (OrthancException& ex)
+      {
+        throw OrthancException(ErrorCode_BadRequest, std::string("Invalid requestedTags argument: ") + ex.What() + " " + ex.GetDetails());
+      }
+    }
+
     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)) // TODO, requestedTags))
     {
       call.GetOutput().AnswerJson(json);
     }
@@ -2845,11 +2865,11 @@
       }
 
       void Answer(RestApiOutput& output,
-                  ServerIndex& index,
+                  ServerContext& context,
                   ResourceType level,
                   bool expand) const
       {
-        AnswerListOfResources(output, index, resources_, level, expand, format_);
+        AnswerListOfResources(output, context, resources_, level, expand, format_);
       }
     };
   }
@@ -2862,6 +2882,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 +2905,13 @@
                          "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 "
@@ -2893,6 +2921,8 @@
 
     ServerContext& context = OrthancRestApi::GetContext(call);
 
+    // MORE_TAGS: TODO: handle RequestedTags
+
     Json::Value request;
     if (!call.ParseJsonRequest(request) ||
         request.type() != Json::objectValue)
@@ -2997,7 +3027,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);
     }
   }
 
@@ -3054,7 +3084,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))
       {
         result.append(resource);
       }
@@ -3169,7 +3199,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))
     {
       call.GetOutput().AnswerJson(resource);
     }
@@ -3541,7 +3571,7 @@
                it = interest.begin(); it != interest.end(); ++it)
         {
           Json::Value item;
-          if (index.ExpandResource(item, *it, level, format))
+          if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format))
           {
             if (metadata)
             {
@@ -3564,7 +3594,7 @@
           ResourceType level;
           Json::Value item;
           if (index.LookupResourceType(level, *it) &&
-              index.ExpandResource(item, *it, level, format))
+              OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format))
           {
             if (metadata)
             {
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Thu Mar 10 19:00:43 2022 +0100
@@ -259,7 +259,7 @@
                        const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
     {
       Json::Value resource;
-      if (context_.GetIndex().ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human))
+      if (context_.ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human))
       {
         if (success_)
         {
--- a/OrthancServer/Sources/ServerContext.cpp	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Mar 10 19:00:43 2022 +0100
@@ -2092,4 +2092,186 @@
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
     isUnknownSopClassAccepted_ = accepted;
   }
+
+
+  static void SerializeExpandedResource(Json::Value& target,
+                                        const ExpandedResource& resource,
+                                        DicomToJsonFormat format)
+  {
+    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);
+    }
+
+  }
+
+
+  bool ServerContext::ExpandResource(Json::Value& target,
+                                     const std::string& publicId,
+                                     ResourceType level,
+                                     DicomToJsonFormat format)
+  {
+    ExpandedResource resource;
+
+    if (GetIndex().ExpandResource(resource, publicId, level, format))
+    {
+      // 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().IsInconsistentDicomTagsLogsEnabled())
+        {
+          LOG(WARNING) << 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 tags might be missing from this answer.";
+        }
+      }
+
+      // MORE_TAGS: TODO: possibly merge missing requested tags from /tags
+
+      SerializeExpandedResource(target, resource, format);
+
+      return true;
+    }
+
+    return false;
+  }
+
 }
--- a/OrthancServer/Sources/ServerContext.h	Thu Mar 10 09:03:24 2022 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Thu Mar 10 19:00:43 2022 +0100
@@ -539,5 +539,11 @@
     bool IsUnknownSopClassAccepted();
 
     void SetUnknownSopClassAccepted(bool accepted);
+
+    bool ExpandResource(Json::Value& target,
+                        const std::string& publicId,
+                        ResourceType level,
+                        DicomToJsonFormat format);
+
   };
 }