changeset 3016:777762336381 db-changes

integration mainline->db-changes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 14 Dec 2018 14:21:27 +0100
parents c0a766e68d6c (current diff) abe49ca61cd5 (diff)
children 517fc4767ae0
files
diffstat 20 files changed, 913 insertions(+), 233 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomFormat/DicomImageInformation.cpp	Tue Dec 11 13:45:47 2018 +0100
+++ b/Core/DicomFormat/DicomImageInformation.cpp	Fri Dec 14 14:21:27 2018 +0100
@@ -20,7 +20,7 @@
  * you do not wish to do so, delete this exception statement from your
  * version. If you delete this exception statement from all source files
  * in the program, then also delete it here.
- * 
+ *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
@@ -195,17 +195,23 @@
       numberOfFrames_ = 1;
     }
 
-    if ((bitsAllocated_ != 8 && bitsAllocated_ != 16 && 
-         bitsAllocated_ != 24 && bitsAllocated_ != 32) ||
-        numberOfFrames_ == 0 ||
-        (planarConfiguration != 0 && planarConfiguration != 1))
+    if (bitsAllocated_ != 8 && bitsAllocated_ != 16 &&
+        bitsAllocated_ != 24 && bitsAllocated_ != 32)
     {
-      throw OrthancException(ErrorCode_NotImplemented);
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: " + boost::lexical_cast<std::string>(bitsAllocated_) + " bits allocated");
+    }
+    else if (numberOfFrames_ == 0)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported (no frames)");
+    }
+    else if (planarConfiguration != 0 && planarConfiguration != 1)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: planar configuration is " + boost::lexical_cast<std::string>(planarConfiguration));
     }
 
     if (samplesPerPixel_ == 0)
     {
-      throw OrthancException(ErrorCode_NotImplemented);
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: samples per pixel is 0");
     }
 
     bytesPerValue_ = bitsAllocated_ / 8;
@@ -279,8 +285,8 @@
       }
     }
 
-    if (GetBitsStored() == 8 && 
-        GetChannelCount() == 3 && 
+    if (GetBitsStored() == 8 &&
+        GetChannelCount() == 3 &&
         !IsSigned() &&
         (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
     {
@@ -294,9 +300,9 @@
 
   size_t DicomImageInformation::GetFrameSize() const
   {
-    return (GetHeight() * 
-            GetWidth() * 
-            GetBytesPerValue() * 
+    return (GetHeight() *
+            GetWidth() *
+            GetBytesPerValue() *
             GetChannelCount());
   }
 }
--- a/Core/DicomFormat/DicomMap.cpp	Tue Dec 11 13:45:47 2018 +0100
+++ b/Core/DicomFormat/DicomMap.cpp	Fri Dec 14 14:21:27 2018 +0100
@@ -982,6 +982,112 @@
   }
 
   
+  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);
+  }    
+
+
+  bool DicomMap::HasOnlyMainDicomTags() const
+  {
+    // TODO - Speed up possible by making this std::set a global variable
+
+    std::set<DicomTag> mainDicomTags;
+    GetMainDicomTags(mainDicomTags);
+
+    for (Map::const_iterator it = map_.begin(); it != map_.end(); ++it)
+    {
+      if (mainDicomTags.find(it->first) == mainDicomTags.end())
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+    
+
   void DicomMap::Serialize(Json::Value& target) const
   {
     target = Json::objectValue;
--- a/Core/DicomFormat/DicomMap.h	Tue Dec 11 13:45:47 2018 +0100
+++ b/Core/DicomFormat/DicomMap.h	Fri Dec 14 14:21:27 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,14 @@
     bool ParseDouble(double& result,
                      const DicomTag& tag) const;
 
+    void FromDicomAsJson(const Json::Value& dicomAsJson);
+
+    void Merge(const DicomMap& other);
+
+    void ExtractMainDicomTags(const DicomMap& other);
+
+    bool HasOnlyMainDicomTags() const;
+    
     void Serialize(Json::Value& target) const;
 
     void Unserialize(const Json::Value& source);
--- a/LinuxCompilation.txt	Tue Dec 11 13:45:47 2018 +0100
+++ b/LinuxCompilation.txt	Fri Dec 14 14:21:27 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	Tue Dec 11 13:45:47 2018 +0100
+++ b/NEWS	Fri Dec 14 14:21:27 2018 +0100
@@ -2,6 +2,20 @@
 ===============================
 
 
+General
+-------
+
+* Optimization: On C-FIND, avoid accessing the storage area whenever possible
+* New configuration option:
+  - "StorageAccessOnFind" to rule the access to the storage area during C-FIND
+
+Maintenance
+-----------
+
+* Removal of the "AllowFindSopClassesInStudy" old configuration option
+* "/tools/create-dicom" is more tolerant wrt. invalid specific character set
+
+
 Version 1.5.0 (2018-12-10)
 ==========================
 
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Fri Dec 14 14:21:27 2018 +0100
@@ -89,82 +89,6 @@
   }
 
 
-  static void ExtractTagFromMainDicomTags(std::set<std::string>& target,
-                                          ServerIndex& index,
-                                          const DicomTag& tag,
-                                          const std::list<std::string>& resources,
-                                          ResourceType level)
-  {
-    for (std::list<std::string>::const_iterator
-           it = resources.begin(); it != resources.end(); ++it)
-    {
-      DicomMap tags;
-      if (index.GetMainDicomTags(tags, *it, level, level) &&
-          tags.HasTag(tag))
-      {
-        target.insert(tags.GetValue(tag).GetContent());
-      }
-    }
-  }
-
-
-  static bool ExtractMetadata(std::set<std::string>& target,
-                              ServerIndex& index,
-                              MetadataType metadata,
-                              const std::list<std::string>& resources)
-  {
-    for (std::list<std::string>::const_iterator
-           it = resources.begin(); it != resources.end(); ++it)
-    {
-      std::string value;
-      if (index.LookupMetadata(value, *it, metadata))
-      {
-        target.insert(value);
-      }
-      else
-      {
-        // This metadata is unavailable for some resource, give up
-        return false;
-      }
-    }
-
-    return true;
-  }  
-
-
-  static void ExtractTagFromInstancesOnDisk(std::set<std::string>& target,
-                                            ServerContext& context,
-                                            const DicomTag& tag,
-                                            const std::list<std::string>& instances)
-  {
-    // WARNING: This function is slow, as it reads the JSON file
-    // summarizing each instance of interest from the hard drive.
-
-    std::string formatted = tag.Format();
-
-    for (std::list<std::string>::const_iterator
-           it = instances.begin(); it != instances.end(); ++it)
-    {
-      Json::Value dicom;
-      context.ReadDicomAsJson(dicom, *it);
-
-      if (dicom.isMember(formatted))
-      {
-        const Json::Value& source = dicom[formatted];
-
-        if (source.type() == Json::objectValue &&
-            source.isMember("Type") &&
-            source.isMember("Value") &&
-            source["Type"].asString() == "String" &&
-            source["Value"].type() == Json::stringValue)
-        {
-          target.insert(source["Value"].asString());
-        }
-      }
-    }
-  }
-
-
   static void ComputePatientCounters(DicomMap& result,
                                      ServerIndex& index,
                                      const std::string& patient,
@@ -230,7 +154,24 @@
     if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
     {
       std::set<std::string> values;
-      ExtractTagFromMainDicomTags(values, index, DICOM_TAG_MODALITY, series, ResourceType_Series);
+
+      for (std::list<std::string>::const_iterator
+             it = series.begin(); it != series.end(); ++it)
+      {
+        DicomMap tags;
+        if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series))
+        {
+          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
+
+          if (value != NULL &&
+              !value->IsNull() &&
+              !value->IsBinary())
+          {
+            values.insert(value->GetContent());
+          }
+        }
+      }
+
       StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values);
     }
 
@@ -253,27 +194,17 @@
     {
       std::set<std::string> values;
 
-      if (ExtractMetadata(values, index, MetadataType_Instance_SopClassUid, instances))
-      {
-        // The metadata "SopClassUid" is available for each of these instances
-        StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
-      }
-      else
+      for (std::list<std::string>::const_iterator
+             it = instances.begin(); it != instances.end(); ++it)
       {
-        OrthancConfiguration::ReaderLock lock;
-
-        if (lock.GetConfiguration().GetBooleanParameter("AllowFindSopClassesInStudy", false))
+        std::string value;
+        if (context.LookupOrReconstructMetadata(value, *it, MetadataType_Instance_SopClassUid))
         {
-          ExtractTagFromInstancesOnDisk(values, context, DICOM_TAG_SOP_CLASS_UID, instances);
-          StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
-        }
-        else
-        {
-          result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, "", false);
-          LOG(WARNING) << "The handling of \"SOP Classes in Study\" (0008,0062) "
-                       << "in C-FIND requests is disabled";
+          values.insert(value);
         }
       }
+
+      StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
     }
   }
 
@@ -365,11 +296,23 @@
 
 
   static void AddAnswer(DicomFindAnswers& answers,
-                        const Json::Value& resource,
+                        const DicomMap& mainDicomTags,
+                        const Json::Value* dicomAsJson,
                         const DicomArray& query,
                         const std::list<DicomTag>& sequencesToReturn,
                         const DicomMap* counters)
   {
+    DicomMap match;
+
+    if (dicomAsJson != NULL)
+    {
+      match.FromDicomAsJson(*dicomAsJson);
+    }
+    else
+    {
+      match.Assign(mainDicomTags);
+    }
+    
     DicomMap result;
 
     for (size_t i = 0; i < query.GetSize(); i++)
@@ -385,16 +328,18 @@
       }
       else
       {
-        std::string tag = query.GetElement(i).GetTag().Format();
-        std::string value;
-        if (resource.isMember(tag))
+        const DicomTag& tag = query.GetElement(i).GetTag();
+        const DicomValue* value = match.TestAndGetValue(tag);
+
+        if (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
         {
-          value = resource.get(tag, Json::arrayValue).get("Value", "").asString();
-          result.SetValue(query.GetElement(i).GetTag(), value, false);
+          result.SetValue(tag, value->GetContent(), false);
         }
         else
         {
-          result.SetValue(query.GetElement(i).GetTag(), "", false);
+          result.SetValue(tag, "", false);
         }
       }
     }
@@ -417,6 +362,11 @@
     {
       answers.Add(result);
     }
+    else if (dicomAsJson == NULL)
+    {
+      LOG(WARNING) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled";
+      answers.Add(result);
+    }
     else
     {
       ParsedDicomFile dicom(result);
@@ -424,7 +374,8 @@
       for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin();
            tag != sequencesToReturn.end(); ++tag)
       {
-        const Json::Value& source = resource[tag->Format()];
+        assert(dicomAsJson != NULL);
+        const Json::Value& source = (*dicomAsJson) [tag->Format()];
 
         if (source.type() == Json::objectValue &&
             source.isMember("Type") &&
@@ -521,6 +472,76 @@
   }
 
 
+  class OrthancFindRequestHandler::LookupVisitor : public LookupResource::IVisitor
+  {
+  private:
+    DicomFindAnswers&           answers_;
+    ServerContext&              context_;
+    ResourceType                level_;
+    const DicomMap&             query_;
+    DicomArray                  queryAsArray_;
+    const std::list<DicomTag>&  sequencesToReturn_;
+
+  public:
+    LookupVisitor(DicomFindAnswers&  answers,
+                  ServerContext& context,
+                  ResourceType level,
+                  const DicomMap& query,
+                  const std::list<DicomTag>& sequencesToReturn) :
+      answers_(answers),
+      context_(context),
+      level_(level),
+      query_(query),
+      queryAsArray_(query),
+      sequencesToReturn_(sequencesToReturn)
+    {
+      answers_.SetComplete(false);
+    }
+
+    virtual bool IsDicomAsJsonNeeded() const
+    {
+      // Ask the "DICOM-as-JSON" attachment only if sequences are to
+      // be returned OR if "query_" contains non-main DICOM tags!
+
+      DicomMap withoutSpecialTags;
+      withoutSpecialTags.Assign(query_);
+
+      // Check out "ComputeCounters()"
+      withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
+      withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY);
+
+      // Check out "AddAnswer()"
+      withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+      withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+      
+      return (!sequencesToReturn_.empty() ||
+              !withoutSpecialTags.HasOnlyMainDicomTags());
+    }
+      
+    virtual void MarkAsComplete()
+    {
+      answers_.SetComplete(true);
+    }
+
+    virtual void Visit(const std::string& publicId,
+                       const std::string& instanceId,
+                       const DicomMap& mainDicomTags,
+                       const Json::Value* dicomAsJson) 
+    {
+      std::auto_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_));
+
+      AddAnswer(answers_, mainDicomTags, dicomAsJson,
+                queryAsArray_, sequencesToReturn_, counters.get());
+    }
+  };
+
+
   void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
                                          const DicomMap& input,
                                          const std::list<DicomTag>& sequencesToReturn,
@@ -623,8 +644,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 +670,9 @@
 
     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	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.h	Fri Dec 14 14:21:27 2018 +0100
@@ -41,6 +41,8 @@
   class OrthancFindRequestHandler : public IFindRequestHandler
   {
   private:
+    class LookupVisitor;
+
     ServerContext& context_;
     unsigned int   maxResults_;
     unsigned int   maxInstances_;
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Fri Dec 14 14:21:27 2018 +0100
@@ -440,6 +440,7 @@
       // Select one existing child instance of the parent resource, to
       // retrieve all its tags
       Json::Value siblingTags;
+      std::string siblingInstanceId;
 
       {
         // Retrieve all the instances of the parent resource
@@ -452,7 +453,8 @@
           throw OrthancException(ErrorCode_InternalError);
         }
 
-        context.ReadDicomAsJson(siblingTags, siblingInstances.front());
+        siblingInstanceId = siblingInstances.front();
+        context.ReadDicomAsJson(siblingTags, siblingInstanceId);
       }
 
 
@@ -463,11 +465,14 @@
         if (siblingTags.isMember(SPECIFIC_CHARACTER_SET))
         {
           Encoding encoding;
+
           if (!siblingTags[SPECIFIC_CHARACTER_SET].isMember("Value") ||
               siblingTags[SPECIFIC_CHARACTER_SET]["Value"].type() != Json::stringValue ||
               !GetDicomEncoding(encoding, siblingTags[SPECIFIC_CHARACTER_SET]["Value"].asCString()))
           {
-            throw OrthancException(ErrorCode_CreateDicomParentEncoding);
+            LOG(WARNING) << "Instance with an incorrect Specific Character Set, "
+                         << "using the default Orthanc encoding: " << siblingInstanceId;
+            encoding = GetDefaultDicomEncoding();
           }
 
           dicom.SetEncoding(encoding);
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri Dec 14 14:21:27 2018 +0100
@@ -1269,6 +1269,49 @@
   }
 
 
+  namespace 
+  {
+    class FindVisitor : public LookupResource::IVisitor
+    {
+    private:
+      bool                    isComplete_;
+      std::list<std::string>  resources_;
+
+    public:
+      FindVisitor() :
+        isComplete_(false)
+      {
+      }
+
+      virtual bool IsDicomAsJsonNeeded() const
+      {
+        return 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 DicomMap& mainDicomTags   /* unused     */,
+                         const Json::Value* dicomAsJson  /* 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 +1324,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 +1380,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 +1393,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 +1409,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 +1418,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	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/Search/LookupIdentifierQuery.cpp	Fri Dec 14 14:21:27 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	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/Search/LookupIdentifierQuery.h	Fri Dec 14 14:21:27 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	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/Search/LookupResource.cpp	Fri Dec 14 14:21:27 2018 +0100
@@ -42,6 +42,19 @@
 
 namespace Orthanc
 {
+  static bool DoesDicomMapMatch(const DicomMap& dicom,
+                                const DicomTag& tag,
+                                const IFindConstraint& constraint)
+  {
+    const DicomValue* value = dicom.TestAndGetValue(tag);
+
+    return (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary() &&
+            constraint.Match(value->GetContent()));
+  }
+
+  
   LookupResource::Level::Level(ResourceType level) : level_(level)
   {
     const DicomTag* tags = NULL;
@@ -113,11 +126,40 @@
     }
     else
     {
+      // This is not a main DICOM tag
       return false;
     }
   }
 
 
+  bool LookupResource::Level::IsMatch(const DicomMap& dicom) const
+  {
+    for (Constraints::const_iterator it = identifiersConstraints_.begin();
+         it != identifiersConstraints_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
+      {
+        return false;
+      }
+    }
+
+    for (Constraints::const_iterator it = mainTagsConstraints_.begin();
+         it != mainTagsConstraints_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+  
+
   LookupResource::LookupResource(ResourceType level) : level_(level)
   {
     switch (level)
@@ -282,22 +324,22 @@
 
 
 
-  bool LookupResource::IsMatch(const Json::Value& dicomAsJson) const
+  bool LookupResource::IsMatch(const DicomMap& dicom) const
   {
+    for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it)
+    {
+      if (!it->second->IsMatch(dicom))
+      {
+        return false;
+      }
+    }
+
     for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); 
          it != unoptimizedConstraints_.end(); ++it)
     {
-      std::string tag = it->first.Format();
-      if (dicomAsJson.isMember(tag) &&
-          dicomAsJson[tag]["Type"] == "String")
-      {
-        std::string value = dicomAsJson[tag]["Value"].asString();
-        if (!it->second->Match(value))
-        {
-          return false;
-        }
-      }
-      else
+      assert(it->second != NULL);
+
+      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
       {
         return false;
       }
--- a/OrthancServer/Search/LookupResource.h	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/Search/LookupResource.h	Fri Dec 14 14:21:27 2018 +0100
@@ -64,13 +64,15 @@
 
       void Apply(SetOfResources& candidates,
                  IDatabaseWrapper& database) const;
+
+      bool IsMatch(const DicomMap& dicom) const;
     };
 
     typedef std::map<ResourceType, Level*>  Levels;
 
     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 +84,23 @@
                     IDatabaseWrapper& database) const;
 
   public:
+    class IVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~IVisitor()
+      {
+      }
+
+      virtual bool IsDicomAsJsonNeeded() const = 0;
+      
+      virtual void MarkAsComplete() = 0;
+
+      virtual void Visit(const std::string& publicId,
+                         const std::string& instanceId,
+                         const DicomMap& mainDicomTags,
+                         const Json::Value* dicomAsJson) = 0;
+    };
+
     LookupResource(ResourceType level);
 
     ~LookupResource();
@@ -103,6 +122,11 @@
     void FindCandidates(std::list<int64_t>& result,
                         IDatabaseWrapper& database) const;
 
-    bool IsMatch(const Json::Value& dicomAsJson) const;
+    bool HasOnlyMainDicomTags() const
+    {
+      return unoptimizedConstraints_.empty();
+    }
+
+    bool IsMatch(const DicomMap& dicom) const;
   };
 }
--- a/OrthancServer/ServerContext.cpp	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/ServerContext.cpp	Fri Dec 14 14:21:27 2018 +0100
@@ -773,27 +773,91 @@
   }
 
 
-  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;
+    LookupMode mode;
+      
+    {
+      // New configuration option in 1.5.1
+      OrthancConfiguration::ReaderLock lock;
+
+      std::string value = lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always");
+
+      if (value == "Always")
+      {
+        mode = LookupMode_DiskOnLookupAndAnswer;
+      }
+      else if (value == "Never")
+      {
+        mode = LookupMode_DatabaseOnly;
+      }
+      else if (value == "Answers")
+      {
+        mode = LookupMode_DiskOnAnswer;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "Configuration option \"StorageAccessOnFind\" "
+                               "should be \"Always\", \"Never\" or \"Answers\": " + value);
+      }
+    }      
+
 
     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;
+
+    const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded();
+    
     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;
-      ReadDicomAsJson(dicom, instances[i]);
+      // Optimization in Orthanc 1.5.1 - Don't read the full JSON from
+      // the disk if only "main DICOM tags" are to be returned
+
+      std::auto_ptr<Json::Value> dicomAsJson;
+
+      bool hasOnlyMainDicomTags;
+      DicomMap dicom;
+      
+      if (mode == LookupMode_DatabaseOnly ||
+          mode == LookupMode_DiskOnAnswer ||
+          lookup.HasOnlyMainDicomTags())
+      {
+        // Case (1): The main DICOM tags, as stored in the database,
+        // are sufficient to look for match
+
+        if (!GetIndex().GetAllMainDicomTags(dicom, instances[i]))
+        {
+          // The instance has been removed during the execution of the
+          // lookup, ignore it
+          continue;
+        }
+        
+        hasOnlyMainDicomTags = true;
+      }
+      else
+      {
+        // Case (2): Need to read the "DICOM-as-JSON" attachment from
+        // the storage area
+        dicomAsJson.reset(new Json::Value);
+        ReadDicomAsJson(*dicomAsJson, instances[i]);
+
+        dicom.FromDicomAsJson(*dicomAsJson);
+
+        // This map contains the entire JSON, i.e. more than the main DICOM tags
+        hasOnlyMainDicomTags = false;   
+      }
       
       if (lookup.IsMatch(dicom))
       {
@@ -802,17 +866,120 @@
           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]);
+          if ((mode == LookupMode_DiskOnLookupAndAnswer ||
+               mode == LookupMode_DiskOnAnswer) &&
+              dicomAsJson.get() == NULL &&
+              isDicomAsJsonNeeded)
+          {
+            dicomAsJson.reset(new Json::Value);
+            ReadDicomAsJson(*dicomAsJson, instances[i]);
+          }
+
+          if (hasOnlyMainDicomTags)
+          {
+            // This is Case (1): The variable "dicom" only contains the main DICOM tags
+            visitor.Visit(resources[i], instances[i], dicom, dicomAsJson.get());
+          }
+          else
+          {
+            // Remove the non-main DICOM tags from "dicom" if Case (2)
+            // was used, for consistency with Case (1)
+
+            DicomMap mainDicomTags;
+            mainDicomTags.ExtractMainDicomTags(dicom);
+            visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get());            
+          }
+            
+          countResults ++;
         }
       }
     }
+
+    if (complete)
+    {
+      visitor.MarkAsComplete();
+    }
+
+    LOG(INFO) << "Number of matching resources: " << countResults;
+  }
+
+
+  bool ServerContext::LookupOrReconstructMetadata(std::string& target,
+                                                  const std::string& publicId,
+                                                  MetadataType metadata)
+  {
+    // This is a backwards-compatibility function, that can
+    // reconstruct metadata that were not generated by an older
+    // release of Orthanc
+
+    if (metadata == MetadataType_Instance_SopClassUid ||
+        metadata == MetadataType_Instance_TransferSyntax)
+    {
+      if (index_.LookupMetadata(target, publicId, metadata))
+      {
+        return true;
+      }
+      else
+      {
+        // These metadata are mandatory in DICOM instances, and were
+        // introduced in Orthanc 1.2.0. The fact that
+        // "LookupMetadata()" has failed indicates that this database
+        // comes from an older release of Orthanc.
+        
+        DicomTag tag(0, 0);
+      
+        switch (metadata)
+        {
+          case MetadataType_Instance_SopClassUid:
+            tag = DICOM_TAG_SOP_CLASS_UID;
+            break;
+
+          case MetadataType_Instance_TransferSyntax:
+            tag = DICOM_TAG_TRANSFER_SYNTAX_UID;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      
+        Json::Value dicomAsJson;
+        ReadDicomAsJson(dicomAsJson, publicId);
+
+        DicomMap tags;
+        tags.FromDicomAsJson(dicomAsJson);
+
+        const DicomValue* value = tags.TestAndGetValue(tag);
+
+        if (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
+        {
+          target = value->GetContent();
+
+          // Store for reuse
+          index_.SetMetadata(publicId, metadata, target);
+          return true;
+        }
+        else
+        {
+          // Should never happen
+          return false;
+        }
+      }
+    }
+    else
+    {
+      // No backward
+      return index_.LookupMetadata(target, publicId, metadata);
+    }
   }
 
 
--- a/OrthancServer/ServerContext.h	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/ServerContext.h	Fri Dec 14 14:21:27 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"
@@ -63,6 +64,14 @@
   class ServerContext : private JobsRegistry::IObserver
   {
   private:
+    enum LookupMode
+    {
+      LookupMode_DatabaseOnly,
+      LookupMode_DiskOnAnswer,
+      LookupMode_DiskOnLookupAndAnswer
+    };
+
+    
     class LuaServerListener : public IServerListener
     {
     private:
@@ -334,12 +343,15 @@
 
     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);
 
+    bool LookupOrReconstructMetadata(std::string& target,
+                                     const std::string& publicId,
+                                     MetadataType type);
+
 
     /**
      * Management of the plugins
--- a/OrthancServer/ServerIndex.cpp	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/ServerIndex.cpp	Fri Dec 14 14:21:27 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	Tue Dec 11 13:45:47 2018 +0100
+++ b/OrthancServer/ServerIndex.h	Fri Dec 14 14:21:27 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/Resources/Configuration.json	Tue Dec 11 13:45:47 2018 +0100
+++ b/Resources/Configuration.json	Fri Dec 14 14:21:27 2018 +0100
@@ -384,14 +384,6 @@
      }
    **/
   
-  // If set to "true", Orthanc will still handle "SOP Classes in
-  // Study" (0008,0062) in C-FIND requests, even if the "SOP Class
-  // UID" metadata is not available in the database (which is the case
-  // if the DB was previously used by Orthanc <= 1.1.0). This option
-  // is turned off by default, as it requires intensive accesses to
-  // the hard drive.
-  "AllowFindSopClassesInStudy" : false,
-
   // If set to "false", Orthanc will not load its default dictionary
   // of private tags. This might be necessary if you cannot import a
   // DICOM file encoded using the Implicit VR Endian transfer syntax,
@@ -447,5 +439,17 @@
   // The least recently used archives get deleted as new archives are
   // generated. This option was introduced in Orthanc 1.5.0, and has
   // no effect on the synchronous generation of archives.
-  "MediaArchiveSize" : 1
+  "MediaArchiveSize" : 1,
+
+  // Performance setting to specify how Orthanc accesses the storage
+  // area during C-FIND. Three modes are available: (1) "Always"
+  // allows Orthanc to read the storage area as soon as it needs an
+  // information that is not present in its database (slowest mode),
+  // (2) "Never" prevents Orthanc from accessing the storage area, and
+  // makes it uses exclusively its database (fastest mode), and (3)
+  // "Answers" allows Orthanc to read the storage area to generate its
+  // answers, but not to filter the DICOM resources (balance between
+  // the two modes). By default, the mode is "Always", which
+  // corresponds to the behavior of Orthanc <= 1.5.0.
+  "StorageAccessOnFind" : "Always"
 }
--- a/UnitTestsSources/DicomMapTests.cpp	Tue Dec 11 13:45:47 2018 +0100
+++ b/UnitTestsSources/DicomMapTests.cpp	Fri Dec 14 14:21:27 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,140 @@
     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);
+  ASSERT_TRUE(b.HasOnlyMainDicomTags());
+
+  {
+    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);
+    ASSERT_FALSE(a.HasOnlyMainDicomTags());
+    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));
+  ASSERT_TRUE(b.HasOnlyMainDicomTags());
+
+  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);
+    ASSERT_FALSE(a.HasOnlyMainDicomTags());
+    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());
+  ASSERT_FALSE(b.HasOnlyMainDicomTags());
+}
--- a/UnitTestsSources/ServerIndexTests.cpp	Tue Dec 11 13:45:47 2018 +0100
+++ b/UnitTestsSources/ServerIndexTests.cpp	Fri Dec 14 14:21:27 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>