changeset 4936:8422e4f99a18 more-tags

Handling RequestedTags in ExpandResource -> read parent main dicom tags if required. Not yet getting missing tags from file. Integration tests ok
author Alain Mazy <am@osimis.io>
date Fri, 11 Mar 2022 17:38:16 +0100
parents acd3f72e2a21
children 3f9b9865c8cc
files OrthancFramework/Sources/DicomFormat/DicomMap.cpp OrthancFramework/Sources/DicomFormat/DicomMap.h OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h OrthancFramework/Sources/Toolbox.h OrthancFramework/UnitTestsSources/ToolboxTests.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h
diffstat 14 files changed, 324 insertions(+), 52 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Fri Mar 11 17:38:16 2022 +0100
@@ -403,7 +403,8 @@
   }
 
 
-  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 std::map<DicomTag, std::string>& mainDicomTags)
   {
@@ -420,10 +421,25 @@
     }
   }
 
+  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);
-    ExtractTags(result, content_, mainDicomTags);
+    ExtractTagsInternal(result, content_, mainDicomTags);
   }
 
   void DicomMap::ExtractPatientInformation(DicomMap& result) const
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Fri Mar 11 17:38:16 2022 +0100
@@ -120,6 +120,8 @@
 
     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);
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Fri Mar 11 17:38:16 2022 +0100
@@ -1332,6 +1332,23 @@
   }
 
 
+  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	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Fri Mar 11 17:38:16 2022 +0100
@@ -178,6 +178,8 @@
     // 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 bool HasTag(const DicomMap& fields,
                        const std::string& tagName);
 
--- a/OrthancFramework/Sources/Toolbox.h	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancFramework/Sources/Toolbox.h	Fri Mar 11 17:38:16 2022 +0100
@@ -185,6 +185,48 @@
                                const std::string& source,
                                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);
+      }
+    }
+
+
 #if ORTHANC_ENABLE_PUGIXML == 1
     static void JsonToXml(std::string& target,
                           const Json::Value& source,
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Fri Mar 11 17:38:16 2022 +0100
@@ -209,3 +209,73 @@
   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));
+  }
+}
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Mar 11 17:38:16 2022 +0100
@@ -713,10 +713,11 @@
   bool StatelessDatabaseOperations::ExpandResource(ExpandedResource& target,
                                                    const std::string& publicId,
                                                    ResourceType level,
-                                                   DicomToJsonFormat format)
+                                                   DicomToJsonFormat format,
+                                                   const std::set<DicomTag>& requestedTags)
   {    
-    class Operations : public ReadOnlyOperationsT5<
-      bool&, ExpandedResource&, const std::string&, ResourceType, DicomToJsonFormat>
+    class Operations : public ReadOnlyOperationsT6<
+      bool&, ExpandedResource&, const std::string&, ResourceType, DicomToJsonFormat, const std::set<DicomTag>&>
     {
     private:
   
@@ -765,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>()) ||
@@ -866,7 +867,58 @@
           // read all tags from DB
           transaction.GetMainDicomTags(target.tags_, internalId);
 
-          // MORE_TAGS: TODO: eventualy get parent dicom tags if requested ....
+          // check if we have access to all requestedTags or if we must get tags from parents
+          const std::set<DicomTag>& requestedTags = tuple.get<5>();
+
+          if (requestedTags.size() > 0)
+          {
+            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)
+            {
+              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;
+            }
+          }
 
           std::string tmp;
 
@@ -903,7 +955,7 @@
 
     bool found;
     Operations operations;
-    operations.Apply(*this, found, target, publicId, level, format);
+    operations.Apply(*this, found, target, publicId, level, format, requestedTags);
     return found;
   }
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Mar 11 17:38:16 2022 +0100
@@ -49,6 +49,7 @@
     std::string                         anonymizedFrom_;
     std::string                         modifiedFrom_;
     std::string                         lastUpdate_;
+    std::set<DicomTag>                  missingRequestedTags_;
 
     // for patients/studies/series
     bool                                isStable_;
@@ -478,7 +479,8 @@
     bool ExpandResource(ExpandedResource& target,
                         const std::string& publicId,
                         ResourceType level,
-                        DicomToJsonFormat format);
+                        DicomToJsonFormat format,
+                        const std::set<DicomTag>& requestedTags);
 
     void GetAllMetadata(std::map<MetadataType, std::string>& target,
                         const std::string& publicId,
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Fri Mar 11 17:38:16 2022 +0100
@@ -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	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Fri Mar 11 17:38:16 2022 +0100
@@ -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	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Mar 11 17:38:16 2022 +0100
@@ -130,7 +130,8 @@
                                     const std::list<std::string>& resources,
                                     ResourceType level,
                                     bool expand,
-                                    DicomToJsonFormat format)
+                                    DicomToJsonFormat format,
+                                    const std::set<DicomTag>& requestedTags)
   {
     Json::Value answer = Json::arrayValue;
 
@@ -140,7 +141,7 @@
       if (expand)
       {
         Json::Value expanded;
-        if (context.ExpandResource(expanded, *resource, level, format))
+        if (context.ExpandResource(expanded, *resource, level, format, requestedTags))
         {
           answer.append(expanded);
         }
@@ -161,6 +162,7 @@
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
+      OrthancRestApi::DocumentRequestedTags(call);
 
       const std::string resources = GetResourceTypeText(resourceType, true /* plural */, false /* lower case */);
       call.GetDocumentation()
@@ -182,6 +184,9 @@
 
     std::list<std::string> result;
 
+    std::set<DicomTag> requestedTags;
+    OrthancRestApi::GetRequestedTags(requestedTags, call);
+
     if (call.HasArgument("limit") ||
         call.HasArgument("since"))
     {
@@ -209,7 +214,8 @@
     }
 
     AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand"),
-                          OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human));
+                          OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human),
+                          requestedTags);
   }
 
 
@@ -220,6 +226,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()
@@ -227,12 +234,6 @@
         .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;
@@ -240,22 +241,12 @@
 
     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());
-      }
-    }
+    std::set<DicomTag> requestedTags;
+    OrthancRestApi::GetRequestedTags(requestedTags, call);
 
     Json::Value json;
     if (OrthancRestApi::GetContext(call).ExpandResource(
-          json, call.GetUriComponent("id", ""), resourceType, format)) // TODO, requestedTags))
+          json, call.GetUriComponent("id", ""), resourceType, format, requestedTags))
     {
       call.GetOutput().AnswerJson(json);
     }
@@ -2867,9 +2858,10 @@
       void Answer(RestApiOutput& output,
                   ServerContext& context,
                   ResourceType level,
-                  bool expand) const
+                  bool expand,
+                  const std::set<DicomTag>& requestedTags) const
       {
-        AnswerListOfResources(output, context, resources_, level, expand, format_);
+        AnswerListOfResources(output, context, resources_, level, expand, format_, requestedTags);
       }
     };
   }
@@ -2911,7 +2903,6 @@
                          "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 "
@@ -2921,8 +2912,6 @@
 
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    // MORE_TAGS: TODO: handle RequestedTags
-
     Json::Value request;
     if (!call.ParseJsonRequest(request) ||
         request.type() != Json::objectValue)
@@ -2960,6 +2949,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;
@@ -3000,6 +2995,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;
@@ -3027,7 +3029,7 @@
 
       FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human));
       context.Apply(visitor, query, level, since, limit);
-      visitor.Answer(call.GetOutput(), context, level, expand);
+      visitor.Answer(call.GetOutput(), context, level, expand, requestedTags);
     }
   }
 
@@ -3039,6 +3041,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 */);
@@ -3055,6 +3058,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", ""));
 
@@ -3084,7 +3090,7 @@
            it = a.begin(); it != a.end(); ++it)
     {
       Json::Value resource;
-      if (OrthancRestApi::GetContext(call).ExpandResource(resource, *it, end, format))
+      if (OrthancRestApi::GetContext(call).ExpandResource(resource, *it, end, format, requestedTags))
       {
         result.append(resource);
       }
@@ -3162,6 +3168,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 */);
@@ -3177,7 +3184,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)
@@ -3199,7 +3209,7 @@
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
 
     Json::Value resource;
-    if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format))
+    if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags))
     {
       call.GetOutput().AnswerJson(resource);
     }
@@ -3438,7 +3448,7 @@
   {
     static const char* const LEVEL = "Level";
     static const char* const METADATA = "Metadata";
-      
+
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
@@ -3450,7 +3460,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)
@@ -3571,7 +3581,9 @@
                it = interest.begin(); it != interest.end(); ++it)
         {
           Json::Value item;
-          if (OrthancRestApi::GetContext(call).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)
             {
@@ -3593,8 +3605,10 @@
         {
           ResourceType level;
           Json::Value item;
+          std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
+
           if (index.LookupResourceType(level, *it) &&
-              OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format))
+              OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags))
           {
             if (metadata)
             {
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Fri Mar 11 17:38:16 2022 +0100
@@ -259,7 +259,9 @@
                        const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
     {
       Json::Value resource;
-      if (context_.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/ServerContext.cpp	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Fri Mar 11 17:38:16 2022 +0100
@@ -2096,7 +2096,8 @@
 
   static void SerializeExpandedResource(Json::Value& target,
                                         const ExpandedResource& resource,
-                                        DicomToJsonFormat format)
+                                        DicomToJsonFormat format,
+                                        const std::set<DicomTag>& requestedTags)
   {
     target = Json::objectValue;
 
@@ -2241,17 +2242,29 @@
       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);
+    }
+
   }
 
 
   bool ServerContext::ExpandResource(Json::Value& target,
                                      const std::string& publicId,
                                      ResourceType level,
-                                     DicomToJsonFormat format)
+                                     DicomToJsonFormat format,
+                                     const std::set<DicomTag>& requestedTags)
   {
     ExpandedResource resource;
 
-    if (GetIndex().ExpandResource(resource, publicId, level, format))
+    if (GetIndex().ExpandResource(resource, publicId, level, format, requestedTags))
     {
       // check the main dicom tags list has not changed since the resource was stored
 
@@ -2260,13 +2273,15 @@
         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.";
+          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 MainDicomTags might be missing from this answer.";
         }
       }
 
       // MORE_TAGS: TODO: possibly merge missing requested tags from /tags
+      // log warning
+      // use resource.missingRequestedTags_
 
-      SerializeExpandedResource(target, resource, format);
+      SerializeExpandedResource(target, resource, format, requestedTags);
 
       return true;
     }
--- a/OrthancServer/Sources/ServerContext.h	Thu Mar 10 19:00:43 2022 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Fri Mar 11 17:38:16 2022 +0100
@@ -543,7 +543,8 @@
     bool ExpandResource(Json::Value& target,
                         const std::string& publicId,
                         ResourceType level,
-                        DicomToJsonFormat format);
+                        DicomToJsonFormat format,
+                        const std::set<DicomTag>& requestedTags);
 
   };
 }