diff OrthancServer/OrthancFindRequestHandler.cpp @ 758:67e6400fca03 query-retrieve

integration mainline -> query-retrieve
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 16 Apr 2014 16:34:09 +0200
parents 3bdb5db8e839 3596177682a9
children c2c28dd17e87
line wrap: on
line diff
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Fri Jan 24 17:40:45 2014 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Wed Apr 16 16:34:09 2014 +0200
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2013 Medical Physics Department, CHU of Liege,
+ * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
  * Belgium
  *
  * This program is free software: you can redistribute it and/or
@@ -48,20 +48,14 @@
             constraint.find('?') != std::string::npos);
   }
 
-  static std::string ToLowerCase(const std::string& s)
-  {
-    std::string result = s;
-    Toolbox::ToLowerCase(result);
-    return result;
-  }
-
   static bool ApplyRangeConstraint(const std::string& value,
                                    const std::string& constraint)
   {
     size_t separator = constraint.find('-');
-    std::string lower = ToLowerCase(constraint.substr(0, separator));
-    std::string upper = ToLowerCase(constraint.substr(separator + 1));
-    std::string v = ToLowerCase(value);
+    std::string lower, upper, v;
+    Toolbox::ToLowerCase(lower, constraint.substr(0, separator));
+    Toolbox::ToLowerCase(upper, constraint.substr(separator + 1));
+    Toolbox::ToLowerCase(v, value);
 
     if (lower.size() == 0 && upper.size() == 0)
     {
@@ -85,15 +79,17 @@
   static bool ApplyListConstraint(const std::string& value,
                                   const std::string& constraint)
   {
-    std::string v1 = ToLowerCase(value);
+    std::string v1;
+    Toolbox::ToLowerCase(v1, value);
 
     std::vector<std::string> items;
     Toolbox::TokenizeString(items, constraint, '\\');
 
     for (size_t i = 0; i < items.size(); i++)
     {
-      Toolbox::ToLowerCase(items[i]);
-      if (items[i] == v1)
+      std::string lower;
+      Toolbox::ToLowerCase(lower, items[i]);
+      if (lower == v1)
       {
         return true;
       }
@@ -129,7 +125,10 @@
     }
     else
     {
-      return ToLowerCase(value) == ToLowerCase(constraint);
+      std::string v, c;
+      Toolbox::ToLowerCase(v, value);
+      Toolbox::ToLowerCase(c, constraint);
+      return v == c;
     }
   }
 
@@ -211,6 +210,10 @@
           value = resource.get(tag, Json::arrayValue).get("Value", "").asString();
           result.SetValue(query.GetElement(i).GetTag(), value);
         }
+        else
+        {
+          result.SetValue(query.GetElement(i).GetTag(), "");
+        }
       }
     }
 
@@ -287,221 +290,152 @@
   }
 
 
-  static bool LookupCandidateResourcesInternal(/* out */ std::list<std::string>& resources,
-                                               /* in */  ServerIndex& index,
-                                               /* in */  ResourceType level,
-                                               /* in */  const DicomMap& query,
-                                               /* in */  DicomTag tag)
-  {
-    if (query.HasTag(tag))
-    {
-      const DicomValue& value = query.GetValue(tag);
-      if (!value.IsNull())
-      {
-        std::string str = query.GetValue(tag).AsString();
-        if (!IsWildcard(str))
-        {
-          index.LookupTagValue(resources, tag, str/*, level*/);
-          return true;
-        }
-      }
-    }
-
-    return false;
-  }
-
-
-  static bool LookupCandidateResourcesInternal(/* inout */ std::set<std::string>& resources,
-                                               /* in */  bool alreadyFiltered,
-                                               /* in */  ServerIndex& index,
-                                               /* in */  ResourceType level,
-                                               /* in */  const DicomMap& query,
-                                               /* in */  DicomTag tag)
+  namespace
   {
-    assert(alreadyFiltered || resources.size() == 0);
-
-    if (!query.HasTag(tag))
-    {
-      return alreadyFiltered;
-    }
-
-    const DicomValue& value = query.GetValue(tag);
-    if (value.IsNull())
-    {
-      return alreadyFiltered;
-    }
-
-    std::string str = query.GetValue(tag).AsString();
-    if (IsWildcard(str))
+    class CandidateResources
     {
-      return alreadyFiltered;
-    }
-
-    std::list<std::string> matches;
-    index.LookupTagValue(matches, tag, str/*, level*/);
+    private:
+      ServerIndex&  index_;
+      ModalityManufacturer manufacturer_;
+      ResourceType  level_;
+      bool  isFilterApplied_;
+      std::set<std::string>  filtered_;
 
-    if (alreadyFiltered)
-    {
-      std::set<std::string> previous = resources;
-      
-      for (std::list<std::string>::const_iterator 
-             it = matches.begin(); it != matches.end(); it++)
+      static void ListToSet(std::set<std::string>& target,
+                            const std::list<std::string>& source)
       {
-        if (previous.find(*it) != previous.end())
+        for (std::list<std::string>::const_iterator
+               it = source.begin(); it != source.end(); ++it)
         {
-          resources.insert(*it);
+          target.insert(*it);
         }
       }
-    }
-    else
-    {
-      for (std::list<std::string>::const_iterator 
-             it = matches.begin(); it != matches.end(); it++)
-      {
-        resources.insert(*it);
-      }
-    }
-
-    return true;
-  }
-
-
-  static bool LookupCandidateResourcesAtOneLevel(/* out */ std::set<std::string>& resources,
-                                                 /* in */  ServerIndex& index,
-                                                 /* in */  ResourceType level,
-                                                 /* in */  const DicomMap& fullQuery,
-                                                 /* in */  ModalityManufacturer manufacturer)
-  {
-    DicomMap tmp;
-    fullQuery.ExtractMainDicomTagsForLevel(tmp, level);
-    DicomArray query(tmp);
-
-    if (query.GetSize() == 0)
-    {
-      return false;
-    }
 
-    for (size_t i = 0; i < query.GetSize(); i++)
-    {
-      const DicomTag tag = query.GetElement(i).GetTag();
-      const DicomValue& value = query.GetElement(i).GetValue();
-      if (!value.IsNull())
+      void ApplyExactFilter(const DicomTag& tag, const std::string& value)
       {
-        // TODO TODO TODO
-      }
-    }
-
-    printf(">>>>>>>>>>\n");
-    query.Print(stdout); 
-    printf("<<<<<<<<<<\n\n");
-    return true;
-  }
+        LOG(INFO) << "Applying exact filter on tag "
+                  << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")";
 
-
-  static void LookupCandidateResources(/* out */ std::list<std::string>& resources,
-                                       /* in */  ServerIndex& index,
-                                       /* in */  ResourceType level,
-                                       /* in */  const DicomMap& query,
-                                       /* in */  ModalityManufacturer manufacturer)
-  {
-#if 1
-    {
-      std::set<std::string> s;
-      LookupCandidateResourcesAtOneLevel(s, index, ResourceType_Patient, query, manufacturer);
-      LookupCandidateResourcesAtOneLevel(s, index, ResourceType_Study, query, manufacturer);
-      LookupCandidateResourcesAtOneLevel(s, index, ResourceType_Series, query, manufacturer);
-      LookupCandidateResourcesAtOneLevel(s, index, ResourceType_Instance, query, manufacturer);
-    }
+        std::list<std::string> resources;
+        index_.LookupTagValue(resources, tag, value, level_);
 
-    std::set<std::string> filtered;
-    bool isFiltered = false; 
-
-    // Filter by indexed tags, from most specific to least specific
-    //isFiltered = LookupCandidateResourcesInternal(filtered, isFiltered, index, level, query, DICOM_TAG_SOP_INSTANCE_UID);
-    isFiltered = LookupCandidateResourcesInternal(filtered, isFiltered, index, level, query, DICOM_TAG_SERIES_INSTANCE_UID);
-    //isFiltered = LookupCandidateResourcesInternal(filtered, isFiltered, index, level, query, DICOM_TAG_STUDY_INSTANCE_UID);
-    //isFiltered = LookupCandidateResourcesInternal(filtered, isFiltered, index, level, query, DICOM_TAG_PATIENT_ID);
-
-    resources.clear();
+        if (isFilterApplied_)
+        {
+          std::set<std::string>  s;
+          ListToSet(s, resources);
 
-    if (isFiltered)
-    {
-      for (std::set<std::string>::const_iterator
-             it = filtered.begin(); it != filtered.end(); it++)
-      {
-        resources.push_back(*it);
-      }
-    }
-    else
-    {
-      // No indexed tag matches the query. Return all the resources at this query level.
-      Json::Value allResources;
-      index.GetAllUuids(allResources, level);
-      assert(allResources.type() == Json::arrayValue);
-
-      for (Json::Value::ArrayIndex i = 0; i < allResources.size(); i++)
-      {
-        resources.push_back(allResources[i].asString());
-      }
-    }
+          std::set<std::string> tmp = filtered_;
+          filtered_.clear();
 
-#else
-
-    // TODO : Speed up using full querying against the MainDicomTags.
-
-    resources.clear();
-
-    bool done = false;
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_PATIENT_ID);
-        break;
-
-      case ResourceType_Study:
-        done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_STUDY_INSTANCE_UID);
-        break;
-
-      case ResourceType_Series:
-        done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_SERIES_INSTANCE_UID);
-        break;
-
-      case ResourceType_Instance:
-        if (manufacturer == ModalityManufacturer_MedInria)
-        {
-          std::list<std::string> series;
-
-          if (LookupCandidateResourcesInternal(series, index, ResourceType_Series, query, DICOM_TAG_SERIES_INSTANCE_UID) &&
-              series.size() == 1)
+          for (std::set<std::string>::const_iterator 
+                 it = tmp.begin(); it != tmp.end(); ++it)
           {
-            index.GetChildInstances(resources, series.front());
-            done = true;
-          }          
+            if (s.find(*it) != s.end())
+            {
+              filtered_.insert(*it);
+            }
+          }
         }
         else
         {
-          done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_SOP_INSTANCE_UID);
+          assert(filtered_.empty());
+          isFilterApplied_ = true;
+          ListToSet(filtered_, resources);
+        }
+      }
+
+    public:
+      CandidateResources(ServerIndex& index,
+                         ModalityManufacturer manufacturer) : 
+        index_(index), 
+        manufacturer_(manufacturer),
+        level_(ResourceType_Patient), 
+        isFilterApplied_(false)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      void GoDown()
+      {
+        assert(level_ != ResourceType_Instance);
+
+        if (isFilterApplied_)
+        {
+          std::set<std::string> tmp = filtered_;
+
+          filtered_.clear();
+
+          for (std::set<std::string>::const_iterator 
+                 it = tmp.begin(); it != tmp.end(); ++it)
+          {
+            std::list<std::string> children;
+            index_.GetChildren(children, *it);
+            ListToSet(filtered_, children);
+          }
         }
 
-        break;
+        switch (level_)
+        {
+          case ResourceType_Patient:
+            level_ = ResourceType_Study;
+            break;
+
+          case ResourceType_Study:
+            level_ = ResourceType_Series;
+            break;
 
-      default:
-        break;
-    }
+          case ResourceType_Series:
+            level_ = ResourceType_Instance;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+
+      void Flatten(std::list<std::string>& resources) const
+      {
+        resources.clear();
 
-    if (!done)
-    {
-      Json::Value allResources;
-      index.GetAllUuids(allResources, level);
-      assert(allResources.type() == Json::arrayValue);
+        if (isFilterApplied_)
+        {
+          for (std::set<std::string>::const_iterator 
+                 it = filtered_.begin(); it != filtered_.end(); ++it)
+          {
+            resources.push_back(*it);
+          }
+        }
+        else
+        {
+          Json::Value tmp;
+          index_.GetAllUuids(tmp, level_);
+          for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
+          {
+            resources.push_back(tmp[i].asString());
+          }
+        }
+      }
 
-      for (Json::Value::ArrayIndex i = 0; i < allResources.size(); i++)
+      void ApplyFilter(const DicomTag& tag, const DicomMap& query)
       {
-        resources.push_back(allResources[i].asString());
+        if (query.HasTag(tag))
+        {
+          const DicomValue& value = query.GetValue(tag);
+          if (!value.IsNull())
+          {
+            std::string value = query.GetValue(tag).AsString();
+            if (!IsWildcard(value))
+            {
+              ApplyExactFilter(tag, value);
+            }
+          }
+        }
       }
-    }
-#endif
+    };
   }
 
 
@@ -538,20 +472,26 @@
 
     ResourceType level = StringToResourceType(levelTmp->AsString().c_str());
 
-    switch (manufacturer)
+    if (level != ResourceType_Patient &&
+        level != ResourceType_Study &&
+        level != ResourceType_Series &&
+        level != ResourceType_Instance)
     {
-      case ModalityManufacturer_MedInria:
-        // MedInria makes FIND requests at the instance level before starting MOVE
-        break;
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
 
-      default:
-        if (level != ResourceType_Patient &&
-            level != ResourceType_Study &&
-            level != ResourceType_Series &&
-            level != ResourceType_Instance)
-        {
-          throw OrthancException(ErrorCode_NotImplemented);
-        }
+    DicomArray query(input);
+    LOG(INFO) << "DICOM C-Find request at level: " << EnumerationToString(level);
+
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      if (!query.GetElement(i).GetValue().IsNull())
+      {
+        LOG(INFO) << "  " << query.GetElement(i).GetTag()
+                  << "  " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag())
+                  << " = " << query.GetElement(i).GetValue().AsString();
+      }
     }
 
 
@@ -562,9 +502,45 @@
      * for each of them.
      **/
 
+    CandidateResources candidates(context_.GetIndex(), manufacturer);
+
+    for (;;)
+    {
+      switch (candidates.GetLevel())
+      {
+        case ResourceType_Patient:
+          candidates.ApplyFilter(DICOM_TAG_PATIENT_ID, input);
+          break;
+
+        case ResourceType_Study:
+          candidates.ApplyFilter(DICOM_TAG_STUDY_INSTANCE_UID, input);
+          candidates.ApplyFilter(DICOM_TAG_ACCESSION_NUMBER, input);
+          break;
+
+        case ResourceType_Series:
+          candidates.ApplyFilter(DICOM_TAG_SERIES_INSTANCE_UID, input);
+          break;
+
+        case ResourceType_Instance:
+          candidates.ApplyFilter(DICOM_TAG_SOP_INSTANCE_UID, input);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }      
+
+      if (candidates.GetLevel() == level)
+      {
+        break;
+      }
+
+      candidates.GoDown();
+    }
+
     std::list<std::string>  resources;
-    LookupCandidateResources(resources, context_.GetIndex(), level, input, manufacturer);
+    candidates.Flatten(resources);
 
+    LOG(INFO) << "Number of candidate resources after exact filtering: " << resources.size();
 
     /**
      * Apply filtering on modalities for studies, if asked (this is an
@@ -587,9 +563,6 @@
      * Loop over all the resources for this query level.
      **/
 
-    DicomArray query(input);
-    query.Print(stdout);
-
     for (std::list<std::string>::const_iterator 
            resource = resources.begin(); resource != resources.end(); ++resource)
     {
@@ -614,3 +587,15 @@
     }
   }
 }
+
+
+
+/**
+ * TODO : Case-insensitive match for PN value representation (Patient
+ * Name). Case-senstive match for all the other value representations.
+ *
+ * Reference: DICOM PS 3.4
+ *   - C.2.2.2.1 ("Single Value Matching") 
+ *   - C.2.2.2.4 ("Wild Card Matching")
+ * http://medical.nema.org/Dicom/2011/11_04pu.pdf (
+ **/