changeset 3008:45a4a1571b4e

merge
author am@osimis.io
date Thu, 13 Dec 2018 15:14:39 +0100
parents c9f93628215a (current diff) 0e1755e5efd0 (diff)
children 750de70b3603 8a15bb3576c2
files
diffstat 17 files changed, 512 insertions(+), 94 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomFormat/DicomMap.cpp	Thu Dec 13 14:48:23 2018 +0100
+++ b/Core/DicomFormat/DicomMap.cpp	Thu Dec 13 15:14:39 2018 +0100
@@ -982,6 +982,93 @@
   }
 
   
+  void DicomMap::FromDicomAsJson(const Json::Value& dicomAsJson)
+  {
+    Clear();
+    
+    Json::Value::Members tags = dicomAsJson.getMemberNames();
+    for (Json::Value::Members::const_iterator
+           it = tags.begin(); it != tags.end(); ++it)
+    {
+      DicomTag tag(0, 0);
+      if (!DicomTag::ParseHexadecimal(tag, it->c_str()))
+      {
+        throw OrthancException(ErrorCode_CorruptedFile);
+      }
+
+      const Json::Value& value = dicomAsJson[*it];
+
+      if (value.type() != Json::objectValue ||
+          !value.isMember("Type") ||
+          !value.isMember("Value") ||
+          value["Type"].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_CorruptedFile);
+      }
+
+      if (value["Type"] == "String")
+      {
+        if (value["Value"].type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_CorruptedFile);
+        }
+        else
+        {
+          SetValue(tag, value["Value"].asString(), false /* not binary */);
+        }
+      }
+    }
+  }
+
+
+  void DicomMap::Merge(const DicomMap& other)
+  {
+    for (Map::const_iterator it = other.map_.begin();
+         it != other.map_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (map_.find(it->first) == map_.end())
+      {
+        map_[it->first] = it->second->Clone();
+      }
+    }
+  }
+
+
+  void DicomMap::ExtractMainDicomTagsInternal(const DicomMap& other,
+                                              ResourceType level)
+  {
+    const DicomTag* tags = NULL;
+    size_t size = 0;
+
+    LoadMainDicomTags(tags, size, level);
+    assert(tags != NULL && size > 0);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      Map::const_iterator found = other.map_.find(tags[i]);
+
+      if (found != other.map_.end() &&
+          map_.find(tags[i]) == map_.end())
+      {
+        assert(found->second != NULL);
+        map_[tags[i]] = found->second->Clone();
+      }
+    }
+  }
+    
+
+  void DicomMap::ExtractMainDicomTags(const DicomMap& other)
+  {
+    Clear();
+    ExtractMainDicomTagsInternal(other, ResourceType_Patient);
+    ExtractMainDicomTagsInternal(other, ResourceType_Study);
+    ExtractMainDicomTagsInternal(other, ResourceType_Series);
+    ExtractMainDicomTagsInternal(other, ResourceType_Instance);
+  }    
+
+
   void DicomMap::Serialize(Json::Value& target) const
   {
     target = Json::objectValue;
--- a/Core/DicomFormat/DicomMap.h	Thu Dec 13 14:48:23 2018 +0100
+++ b/Core/DicomFormat/DicomMap.h	Thu Dec 13 15:14:39 2018 +0100
@@ -59,7 +59,7 @@
                   uint16_t element, 
                   DicomValue* value);
 
-    void SetValue(DicomTag tag, 
+    void SetValue(DicomTag tag,
                   DicomValue* value);
 
     void ExtractTags(DicomMap& source,
@@ -68,6 +68,9 @@
    
     static void GetMainDicomTagsInternal(std::set<DicomTag>& result, ResourceType level);
 
+    void ExtractMainDicomTagsInternal(const DicomMap& other,
+                                      ResourceType level);
+
   public:
     DicomMap()
     {
@@ -217,6 +220,12 @@
     bool ParseDouble(double& result,
                      const DicomTag& tag) const;
 
+    void FromDicomAsJson(const Json::Value& dicomAsJson);
+
+    void Merge(const DicomMap& other);
+
+    void ExtractMainDicomTags(const DicomMap& other); 
+    
     void Serialize(Json::Value& target) const;
 
     void Unserialize(const Json::Value& source);
--- a/LinuxCompilation.txt	Thu Dec 13 14:48:23 2018 +0100
+++ b/LinuxCompilation.txt	Thu Dec 13 15:14:39 2018 +0100
@@ -115,8 +115,8 @@
 
 
 
-SUPPORTED - Ubuntu 14.04 LTS
-----------------------------
+SUPPORTED - Ubuntu 14.04 LTS and 16.04 LTS
+------------------------------------------
 
 # sudo apt-get install build-essential unzip cmake mercurial \
        	       	       uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
--- a/NEWS	Thu Dec 13 14:48:23 2018 +0100
+++ b/NEWS	Thu Dec 13 15:14:39 2018 +0100
@@ -28,7 +28,7 @@
 * New URI: "/studies/.../merge" to merge a study
 * New URI: "/studies/.../split" to split a study
 * POST-ing a DICOM file to "/instances" also answers the patient/study/series ID
-* modalities returned by GET "/modalities?expand" and "/modalities/..." are now JSON objects instead of JSON arrays
+* GET "/modalities/?expand" now returns a JSON object instead of a JSON array
 * New "Details" field in HTTP answers on error (cf. "HttpDescribeErrors" option)
 * New options to URI "/queries/.../answers": "?expand" and "?simplify"
 * New URIs to launch new C-FIND to explore the hierarchy of a C-FIND answer:
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Thu Dec 13 15:14:39 2018 +0100
@@ -521,6 +521,47 @@
   }
 
 
+  class OrthancFindRequestHandler::LookupVisitor : public LookupResource::IVisitor
+  {
+  private:
+    DicomFindAnswers&           answers_;
+    ServerContext&              context_;
+    ResourceType                level_;
+    const DicomMap&             filteredInput_;
+    const std::list<DicomTag>&  sequencesToReturn_;
+    DicomArray                  query_;
+
+  public:
+    LookupVisitor(DicomFindAnswers&  answers,
+                  ServerContext& context,
+                  ResourceType level,
+                  const DicomMap& filteredInput,
+                  const std::list<DicomTag>& sequencesToReturn) :
+      answers_(answers),
+      context_(context),
+      level_(level),
+      filteredInput_(filteredInput),
+      sequencesToReturn_(sequencesToReturn),
+      query_(filteredInput)
+    {
+      answers_.SetComplete(false);
+    }
+      
+    virtual void MarkAsComplete()
+    {
+      answers_.SetComplete(true);
+    }
+
+    virtual void Visit(const std::string& publicId,
+                       const std::string& instanceId,
+                       const Json::Value& dicom) 
+    {
+      std::auto_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, filteredInput_));
+      AddAnswer(answers_, dicom, query_, sequencesToReturn_, counters.get());
+    }
+  };
+
+
   void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
                                          const DicomMap& input,
                                          const std::list<DicomTag>& sequencesToReturn,
@@ -623,8 +664,6 @@
 
       if (FilterQueryTag(value, level, tag, manufacturer))
       {
-        // TODO - Move this to "ResourceLookup::AddDicomConstraint()"
-
         ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
 
         // DICOM specifies that searches must be case sensitive, except
@@ -651,42 +690,8 @@
 
     size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;
 
-    // TODO - Use ServerContext::Apply() at this point, in order to
-    // share the code with the "/tools/find" REST URI
-    std::vector<std::string> resources, instances;
-    context_.GetIndex().FindCandidates(resources, instances, lookup);
-
-    LOG(INFO) << "Number of candidate resources after fast DB filtering: " << resources.size();
-
-    assert(resources.size() == instances.size());
-    bool complete = true;
-
-    for (size_t i = 0; i < instances.size(); i++)
-    {
-      // TODO - Don't read the full JSON from the disk if only "main
-      // DICOM tags" are to be returned
-      Json::Value dicom;
-      context_.ReadDicomAsJson(dicom, instances[i]);
-      
-      if (lookup.IsMatch(dicom))
-      {
-        if (limit != 0 &&
-            answers.GetSize() >= limit)
-        {
-          complete = false;
-          break;
-        }
-        else
-        {
-          std::auto_ptr<DicomMap> counters(ComputeCounters(context_, instances[i], level, *filteredInput));
-          AddAnswer(answers, dicom, query, sequencesToReturn, counters.get());
-        }
-      }
-    }
-
-    LOG(INFO) << "Number of matching resources: " << answers.GetSize();
-
-    answers.SetComplete(complete);
+    LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn);
+    context_.Apply(visitor, lookup, 0 /* "since" is not relevant to C-FIND */, limit);
   }
 
 
--- a/OrthancServer/OrthancFindRequestHandler.h	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.h	Thu Dec 13 15:14:39 2018 +0100
@@ -41,6 +41,8 @@
   class OrthancFindRequestHandler : public IFindRequestHandler
   {
   private:
+    class LookupVisitor;
+
     ServerContext& context_;
     unsigned int   maxResults_;
     unsigned int   maxInstances_;
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Dec 13 15:14:39 2018 +0100
@@ -1269,6 +1269,43 @@
   }
 
 
+  namespace 
+  {
+    class FindVisitor : public LookupResource::IVisitor
+    {
+    private:
+      bool                    isComplete_;
+      std::list<std::string>  resources_;
+
+    public:
+      FindVisitor() :
+        isComplete_(false)
+      {
+      }
+
+      virtual void MarkAsComplete()
+      {
+        isComplete_ = true;  // Unused information as of Orthanc 1.5.0
+      }
+
+      virtual void Visit(const std::string& publicId,
+                         const std::string& instanceId  /* unused */,  
+                         const Json::Value& dicom       /* unused */)
+      {
+        resources_.push_back(publicId);
+      }
+
+      void Answer(RestApiOutput& output,
+                  ServerIndex& index,
+                  ResourceType level,
+                  bool expand) const
+      {
+        AnswerListOfResources(output, index, resources_, level, expand);
+      }
+    };
+  }
+
+
   static void Find(RestApiPostCall& call)
   {
     static const char* const KEY_CASE_SENSITIVE = "CaseSensitive";
@@ -1281,15 +1318,43 @@
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     Json::Value request;
-    if (call.ParseJsonRequest(request) &&
-        request.type() == Json::objectValue &&
-        request.isMember(KEY_LEVEL) &&
-        request.isMember(KEY_QUERY) &&
-        request[KEY_LEVEL].type() == Json::stringValue &&
-        request[KEY_QUERY].type() == Json::objectValue &&
-        (!request.isMember(KEY_CASE_SENSITIVE) || request[KEY_CASE_SENSITIVE].type() == Json::booleanValue) &&
-        (!request.isMember(KEY_LIMIT) || request[KEY_LIMIT].type() == Json::intValue) &&
-        (!request.isMember(KEY_SINCE) || request[KEY_SINCE].type() == Json::intValue))
+    if (!call.ParseJsonRequest(request) ||
+        request.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "The body must contain a JSON object");
+    }
+    else if (!request.isMember(KEY_LEVEL) ||
+             request[KEY_LEVEL].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_LEVEL) + "\" is missing, or should be a string");
+    }
+    else if (!request.isMember(KEY_QUERY) &&
+             request[KEY_QUERY].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_QUERY) + "\" is missing, or should be a JSON object");
+    }
+    else if (request.isMember(KEY_CASE_SENSITIVE) && 
+             request[KEY_CASE_SENSITIVE].type() != Json::booleanValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean");
+    }
+    else if (request.isMember(KEY_LIMIT) && 
+             request[KEY_LIMIT].type() != Json::intValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_LIMIT) + "\" should be an integer");
+    }
+    else if (request.isMember(KEY_SINCE) &&
+             request[KEY_SINCE].type() != Json::intValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_SINCE) + "\" should be an integer");
+    }
+    else
     {
       bool expand = false;
       if (request.isMember(KEY_EXPAND))
@@ -1309,7 +1374,8 @@
         int tmp = request[KEY_LIMIT].asInt();
         if (tmp < 0)
         {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer");
         }
 
         limit = static_cast<size_t>(tmp);
@@ -1321,7 +1387,8 @@
         int tmp = request[KEY_SINCE].asInt();
         if (tmp < 0)
         {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer");
         }
 
         since = static_cast<size_t>(tmp);
@@ -1336,7 +1403,8 @@
       {
         if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
         {
-          throw OrthancException(ErrorCode_BadRequest);
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Tag \"" + members[i] + "\" should be associated with a string");
         }
 
         query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), 
@@ -1344,15 +1412,9 @@
                                  caseSensitive);
       }
 
-      bool isComplete;
-      std::list<std::string> resources;
-      context.Apply(isComplete, resources, query, since, limit);
-      AnswerListOfResources(call.GetOutput(), context.GetIndex(),
-                            resources, query.GetLevel(), expand);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadRequest);
+      FindVisitor visitor;
+      context.Apply(visitor, query, since, limit);
+      visitor.Answer(call.GetOutput(), context.GetIndex(), query.GetLevel(), expand);
     }
   }
 
--- a/OrthancServer/Search/LookupIdentifierQuery.cpp	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/Search/LookupIdentifierQuery.cpp	Thu Dec 13 15:14:39 2018 +0100
@@ -34,9 +34,10 @@
 #include "../PrecompiledHeadersServer.h"
 #include "LookupIdentifierQuery.h"
 
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/OrthancException.h"
+#include "../ServerToolbox.h"
 #include "SetOfResources.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 
 #include <cassert>
 
@@ -44,6 +45,28 @@
 
 namespace Orthanc
 {
+  LookupIdentifierQuery::SingleConstraint::
+  SingleConstraint(const DicomTag& tag,
+                   IdentifierConstraintType type,
+                   const std::string& value) : 
+    tag_(tag),
+    type_(type),
+    value_(ServerToolbox::NormalizeIdentifier(value))
+  {
+  }
+
+
+  LookupIdentifierQuery::RangeConstraint::
+  RangeConstraint(const DicomTag& tag,
+                  const std::string& start,
+                  const std::string& end) : 
+    tag_(tag),
+    start_(ServerToolbox::NormalizeIdentifier(start)),
+    end_(ServerToolbox::NormalizeIdentifier(end))
+  {
+  }
+
+
   LookupIdentifierQuery::Disjunction::~Disjunction()
   {
     for (size_t i = 0; i < singleConstraints_.size(); i++)
@@ -84,6 +107,12 @@
   }
 
 
+  bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag)
+  {
+    return ServerToolbox::IsIdentifier(tag, level_);
+  }
+
+
   void LookupIdentifierQuery::AddConstraint(DicomTag tag,
                                             IdentifierConstraintType type,
                                             const std::string& value)
--- a/OrthancServer/Search/LookupIdentifierQuery.h	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/Search/LookupIdentifierQuery.h	Thu Dec 13 15:14:39 2018 +0100
@@ -33,7 +33,6 @@
 
 #pragma once
 
-#include "../ServerToolbox.h"
 #include "../IDatabaseWrapper.h"
 
 #include "SetOfResources.h"
@@ -79,12 +78,7 @@
     public:
       SingleConstraint(const DicomTag& tag,
                        IdentifierConstraintType type,
-                       const std::string& value) : 
-        tag_(tag),
-        type_(type),
-        value_(ServerToolbox::NormalizeIdentifier(value))
-      {
-      }
+                       const std::string& value);
 
       const DicomTag& GetTag() const
       {
@@ -113,12 +107,7 @@
     public:
       RangeConstraint(const DicomTag& tag,
                       const std::string& start,
-                      const std::string& end) : 
-        tag_(tag),
-        start_(ServerToolbox::NormalizeIdentifier(start)),
-        end_(ServerToolbox::NormalizeIdentifier(end))
-      {
-      }
+                      const std::string& end);
 
       const DicomTag& GetTag() const
       {
@@ -189,10 +178,7 @@
 
     ~LookupIdentifierQuery();
 
-    bool IsIdentifier(const DicomTag& tag)
-    {
-      return ServerToolbox::IsIdentifier(tag, level_);
-    }
+    bool IsIdentifier(const DicomTag& tag);
 
     void AddConstraint(DicomTag tag,
                        IdentifierConstraintType type,
--- a/OrthancServer/Search/LookupResource.cpp	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/Search/LookupResource.cpp	Thu Dec 13 15:14:39 2018 +0100
@@ -113,6 +113,7 @@
     }
     else
     {
+      // This is not a main DICOM tag
       return false;
     }
   }
--- a/OrthancServer/Search/LookupResource.h	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/Search/LookupResource.h	Thu Dec 13 15:14:39 2018 +0100
@@ -70,7 +70,7 @@
 
     ResourceType                    level_;
     Levels                          levels_;
-    Constraints                     unoptimizedConstraints_; 
+    Constraints                     unoptimizedConstraints_;   // Constraints on non-main DICOM tags
     std::auto_ptr<ListConstraint>   modalitiesInStudy_;
 
     bool AddInternal(ResourceType level,
@@ -82,6 +82,20 @@
                     IDatabaseWrapper& database) const;
 
   public:
+    class IVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~IVisitor()
+      {
+      }
+
+      virtual void MarkAsComplete() = 0;
+
+      virtual void Visit(const std::string& publicId,
+                         const std::string& instanceId,
+                         const Json::Value& dicom) = 0;
+    };
+
     LookupResource(ResourceType level);
 
     ~LookupResource();
--- a/OrthancServer/ServerContext.cpp	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/ServerContext.cpp	Thu Dec 13 15:14:39 2018 +0100
@@ -773,21 +773,22 @@
   }
 
 
-  void ServerContext::Apply(bool& isComplete, 
-                            std::list<std::string>& result,
+  void ServerContext::Apply(LookupResource::IVisitor& visitor,
                             const ::Orthanc::LookupResource& lookup,
                             size_t since,
                             size_t limit)
   {
-    result.clear();
-    isComplete = true;
-
     std::vector<std::string> resources, instances;
     GetIndex().FindCandidates(resources, instances, lookup);
 
+    LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size();
+
     assert(resources.size() == instances.size());
 
+    size_t countResults = 0;
     size_t skipped = 0;
+    bool complete = true;
+
     for (size_t i = 0; i < instances.size(); i++)
     {
       // TODO - Don't read the full JSON from the disk if only "main
@@ -802,17 +803,26 @@
           skipped++;
         }
         else if (limit != 0 &&
-                 result.size() >= limit)
+                 countResults >= limit)
         {
-          isComplete = false;
-          return;  // too many results
+          // Too many results, don't mark as complete
+          complete = false;
+          break;
         }
         else
         {
-          result.push_back(resources[i]);
+          visitor.Visit(resources[i], instances[i], dicom);
+          countResults ++;
         }
       }
     }
+
+    if (complete)
+    {
+      visitor.MarkAsComplete();
+    }
+
+    LOG(INFO) << "Number of matching resources: " << countResults;
   }
 
 
--- a/OrthancServer/ServerContext.h	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/ServerContext.h	Thu Dec 13 15:14:39 2018 +0100
@@ -38,6 +38,7 @@
 #include "LuaScripting.h"
 #include "OrthancHttpHandler.h"
 #include "ServerIndex.h"
+#include "Search/LookupResource.h"
 
 #include "../Core/Cache/MemoryCache.h"
 #include "../Core/Cache/SharedArchive.h"
@@ -334,8 +335,7 @@
 
     void Stop();
 
-    void Apply(bool& isComplete, 
-               std::list<std::string>& result,
+    void Apply(LookupResource::IVisitor& visitor,
                const ::Orthanc::LookupResource& lookup,
                size_t since,
                size_t limit);
--- a/OrthancServer/ServerIndex.cpp	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/ServerIndex.cpp	Thu Dec 13 15:14:39 2018 +0100
@@ -2248,6 +2248,78 @@
   }
 
 
+  bool ServerIndex::GetAllMainDicomTags(DicomMap& result,
+                                        const std::string& instancePublicId)
+  {
+    result.Clear();
+    
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t instance;
+    ResourceType type;
+    if (!db_.LookupResource(instance, type, instancePublicId) ||
+        type != ResourceType_Instance)
+    {
+      return false;
+    }
+    else
+    {
+      DicomMap tmp;
+
+      db_.GetMainDicomTags(tmp, instance);
+      result.Merge(tmp);
+
+      int64_t series;
+      if (!db_.LookupParent(series, instance))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      tmp.Clear();
+      db_.GetMainDicomTags(tmp, series);
+      result.Merge(tmp);
+
+      int64_t study;
+      if (!db_.LookupParent(study, series))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      tmp.Clear();
+      db_.GetMainDicomTags(tmp, study);
+      result.Merge(tmp);
+
+#ifndef NDEBUG
+      {
+        // Sanity test to check that all the main DICOM tags from the
+        // patient level are copied at the study level
+        
+        int64_t patient;
+        if (!db_.LookupParent(patient, study))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        tmp.Clear();
+        db_.GetMainDicomTags(tmp, study);
+
+        std::set<DicomTag> patientTags;
+        tmp.GetTags(patientTags);
+
+        for (std::set<DicomTag>::const_iterator
+               it = patientTags.begin(); it != patientTags.end(); ++it)
+        {
+          assert(result.HasTag(*it));
+        }
+      }
+#endif
+      
+      return true;
+    }
+  }
+
+
   bool ServerIndex::LookupResourceType(ResourceType& type,
                                        const std::string& publicId)
   {
--- a/OrthancServer/ServerIndex.h	Thu Dec 13 14:48:23 2018 +0100
+++ b/OrthancServer/ServerIndex.h	Thu Dec 13 15:14:39 2018 +0100
@@ -275,6 +275,10 @@
                           ResourceType expectedType,
                           ResourceType levelOfInterest);
 
+    // Only applicable at the instance level
+    bool GetAllMainDicomTags(DicomMap& result,
+                             const std::string& instancePublicId);
+
     bool LookupResourceType(ResourceType& type,
                             const std::string& publicId);
 
--- a/UnitTestsSources/DicomMapTests.cpp	Thu Dec 13 14:48:23 2018 +0100
+++ b/UnitTestsSources/DicomMapTests.cpp	Thu Dec 13 15:14:39 2018 +0100
@@ -37,8 +37,12 @@
 #include "../Core/OrthancException.h"
 #include "../Core/DicomFormat/DicomMap.h"
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+
+#include "../OrthancServer/DicomInstanceToStore.h"
 
 #include <memory>
+#include <dcmtk/dcmdata/dcdeftag.h>
 
 using namespace Orthanc;
 
@@ -409,3 +413,135 @@
     ASSERT_THROW(v->GetContent(), OrthancException);
   }
 }
+
+
+
+TEST(DicomMap, DicomAsJson)
+{
+  // This is a Latin-1 test string: "crane" with a circumflex accent
+  const unsigned char raw[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 };
+  std::string latin1((char*) &raw[0], sizeof(raw) / sizeof(char));
+
+  std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1);
+
+  ParsedDicomFile dicom(false);
+  dicom.SetEncoding(Encoding_Latin1);
+  dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Hello");
+  dicom.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, utf8);
+  dicom.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, std::string(ORTHANC_MAXIMUM_TAG_LENGTH, 'a'));
+  dicom.ReplacePlainString(DICOM_TAG_MANUFACTURER, std::string(ORTHANC_MAXIMUM_TAG_LENGTH + 1, 'a'));
+  dicom.ReplacePlainString(DICOM_TAG_PIXEL_DATA, "binary");
+  dicom.ReplacePlainString(DICOM_TAG_ROWS, "512");
+
+  DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
+  dataset.insertEmptyElement(DCM_StudyID, OFFalse);
+
+  {
+    std::auto_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_ReferencedSeriesSequence));
+
+    {
+      std::auto_ptr<DcmItem> item(new DcmItem);
+      item->putAndInsertString(DCM_ReferencedSOPInstanceUID, "nope", OFFalse);
+      ASSERT_TRUE(sequence->insert(item.release(), false, false).good());
+    }
+
+    ASSERT_TRUE(dataset.insert(sequence.release(), false, false).good());
+  }
+  
+                          
+  // Check re-encoding
+  DcmElement* element = NULL;
+  ASSERT_TRUE(dataset.findAndGetElement(DCM_StudyDescription, element).good() &&
+              element != NULL);
+
+  char* c = NULL;
+  ASSERT_TRUE(element != NULL &&
+              element->isLeaf() &&
+              element->isaString() &&
+              element->getString(c).good());
+  ASSERT_EQ(0, memcmp(c, raw, latin1.length()));
+
+  ASSERT_TRUE(dataset.findAndGetElement(DCM_Rows, element).good() &&
+              element != NULL &&
+              element->getTag().getEVR() == EVR_US);
+
+  DicomInstanceToStore toStore;
+  toStore.SetParsedDicomFile(dicom);
+
+  DicomMap m;
+  m.FromDicomAsJson(toStore.GetJson());
+
+  ASSERT_EQ("ISO_IR 100", m.GetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET).GetContent());
+  
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).IsBinary());
+  ASSERT_EQ("Hello", m.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
+  
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).IsBinary());
+  ASSERT_EQ(utf8, m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
+
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_MANUFACTURER));                // Too long
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA));                  // Pixel data
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE));  // Sequence
+  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetGroup(), DCM_ReferencedSeriesSequence.getGroup());
+  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetElement(), DCM_ReferencedSeriesSequence.getElement());
+
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_DESCRIPTION));  // Maximum length
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).IsBinary());
+  ASSERT_EQ(ORTHANC_MAXIMUM_TAG_LENGTH, m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent().length());
+
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_ROWS).IsBinary());
+  ASSERT_EQ("512", m.GetValue(DICOM_TAG_ROWS).GetContent());
+
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsNull());
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsBinary());
+  ASSERT_EQ("", m.GetValue(DICOM_TAG_STUDY_ID).GetContent());
+
+  DicomArray a(m);
+  ASSERT_EQ(6u, a.GetSize());
+
+  
+  //dicom.SaveToFile("/tmp/test.dcm"); 
+  //std::cout << toStore.GetJson() << std::endl;
+  //a.Print(stdout);
+}
+
+
+
+TEST(DicomMap, ExtractMainDicomTags)
+{
+  DicomMap b;
+  b.SetValue(DICOM_TAG_PATIENT_NAME, "E", false);
+
+  {
+    DicomMap a;
+    a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false);
+    a.SetValue(DICOM_TAG_STUDY_DESCRIPTION, "B", false);
+    a.SetValue(DICOM_TAG_SERIES_DESCRIPTION, "C", false);
+    a.SetValue(DICOM_TAG_NUMBER_OF_FRAMES, "D", false);
+    a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false);
+    b.ExtractMainDicomTags(a);
+  }
+
+  ASSERT_EQ(4u, b.GetSize());
+  ASSERT_EQ("A", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
+  ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
+  ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent());
+  ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
+  ASSERT_FALSE(b.HasTag(DICOM_TAG_SLICE_THICKNESS));
+
+  b.SetValue(DICOM_TAG_PATIENT_NAME, "G", false);
+
+  {
+    DicomMap a;
+    a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false);
+    a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false);
+    b.Merge(a);
+  }
+
+  ASSERT_EQ(5u, b.GetSize());
+  ASSERT_EQ("G", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
+  ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
+  ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent());
+  ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
+  ASSERT_EQ("F", b.GetValue(DICOM_TAG_SLICE_THICKNESS).GetContent());
+}
--- a/UnitTestsSources/ServerIndexTests.cpp	Thu Dec 13 14:48:23 2018 +0100
+++ b/UnitTestsSources/ServerIndexTests.cpp	Thu Dec 13 15:14:39 2018 +0100
@@ -38,9 +38,10 @@
 #include "../Core/FileStorage/MemoryStorageArea.h"
 #include "../Core/Logging.h"
 #include "../OrthancServer/DatabaseWrapper.h"
+#include "../OrthancServer/Search/LookupIdentifierQuery.h"
 #include "../OrthancServer/ServerContext.h"
 #include "../OrthancServer/ServerIndex.h"
-#include "../OrthancServer/Search/LookupIdentifierQuery.h"
+#include "../OrthancServer/ServerToolbox.h"
 
 #include <ctype.h>
 #include <algorithm>