changeset 1360:0649c5aef34a

DicomFindQuery
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 15 May 2015 15:34:32 +0200
parents 4378a6636187
children 94ffb597d297
files CMakeLists.txt Core/DicomFormat/DicomMap.cpp Core/DicomFormat/DicomMap.h OrthancServer/DicomFindQuery.cpp OrthancServer/DicomFindQuery.h OrthancServer/FromDcmtkBridge.cpp OrthancServer/FromDcmtkBridge.h OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/ResourceFinder.cpp OrthancServer/ResourceFinder.h UnitTestsSources/DicomMapTests.cpp UnitTestsSources/FromDcmtkTests.cpp
diffstat 12 files changed, 617 insertions(+), 141 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Fri May 15 13:17:37 2015 +0200
+++ b/CMakeLists.txt	Fri May 15 15:34:32 2015 +0200
@@ -171,6 +171,7 @@
   OrthancServer/OrthancMoveRequestHandler.cpp
   OrthancServer/ExportedResource.cpp
   OrthancServer/ResourceFinder.cpp
+  OrthancServer/DicomFindQuery.cpp
 
   # From "lua-scripting" branch
   OrthancServer/DicomInstanceToStore.cpp
--- a/Core/DicomFormat/DicomMap.cpp	Fri May 15 13:17:37 2015 +0200
+++ b/Core/DicomFormat/DicomMap.cpp	Fri May 15 15:34:32 2015 +0200
@@ -406,4 +406,16 @@
     DicomArray a(*this);
     a.Print(fp);
   }
+
+
+  void DicomMap::GetTags(std::set<DicomTag>& tags) const
+  {
+    tags.clear();
+
+    for (Map::const_iterator it = map_.begin();
+         it != map_.end(); ++it)
+    {
+      tags.insert(it->first);
+    }
+  }
 }
--- a/Core/DicomFormat/DicomMap.h	Fri May 15 13:17:37 2015 +0200
+++ b/Core/DicomFormat/DicomMap.h	Fri May 15 15:34:32 2015 +0200
@@ -171,5 +171,7 @@
     static void GetMainDicomTags(std::set<DicomTag>& result);
 
     void Print(FILE* fp) const;
+
+    void GetTags(std::set<DicomTag>& tags) const;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomFindQuery.cpp	Fri May 15 15:34:32 2015 +0200
@@ -0,0 +1,358 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+#include "PrecompiledHeadersServer.h"
+#include "DicomFindQuery.h"
+
+#include "FromDcmtkBridge.h"
+
+#include <boost/regex.hpp> 
+
+
+namespace Orthanc
+{
+  class DicomFindQuery::ValueConstraint : public DicomFindQuery::IConstraint
+  {
+  private:
+    bool          isCaseSensitive_;
+    std::string   expected_;
+
+  public:
+    ValueConstraint(const std::string& value,
+                    bool caseSensitive) :
+      isCaseSensitive_(caseSensitive),
+      expected_(value)
+    {
+    }
+
+    const std::string& GetValue() const
+    {
+      return expected_;
+    }
+
+    virtual bool IsExactConstraint() const
+    {
+      return isCaseSensitive_;
+    }
+
+    virtual bool Apply(const std::string& value) const
+    {
+      if (isCaseSensitive_)
+      {
+        return expected_ == value;
+      }
+      else
+      {
+        std::string v, c;
+        Toolbox::ToLowerCase(v, value);
+        Toolbox::ToLowerCase(c, expected_);
+        return v == c;
+      }
+    }
+  };
+
+
+  class DicomFindQuery::ListConstraint : public DicomFindQuery::IConstraint
+  {
+  private:
+    std::set<std::string>  values_;
+
+  public:
+    ListConstraint(const std::string& values)
+    {
+      std::vector<std::string> items;
+      Toolbox::TokenizeString(items, values, '\\');
+
+      for (size_t i = 0; i < items.size(); i++)
+      {
+        std::string lower;
+        Toolbox::ToLowerCase(lower, items[i]);
+        values_.insert(lower);
+      }
+    }
+
+    virtual bool Apply(const std::string& value) const
+    {
+      std::string tmp;
+      Toolbox::ToLowerCase(tmp, value);
+      return values_.find(tmp) != values_.end();
+    }
+  };
+
+
+  class DicomFindQuery::RangeConstraint : public DicomFindQuery::IConstraint
+  {
+  private:
+    std::string lower_;
+    std::string upper_;
+
+  public:
+    RangeConstraint(const std::string& range)
+    {
+      size_t separator = range.find('-');
+      Toolbox::ToLowerCase(lower_, range.substr(0, separator));
+      Toolbox::ToLowerCase(upper_, range.substr(separator + 1));
+    }
+
+    virtual bool Apply(const std::string& value) const
+    {
+      std::string v;
+      Toolbox::ToLowerCase(v, value);
+
+      if (lower_.size() == 0 && 
+          upper_.size() == 0)
+      {
+        return false;
+      }
+
+      if (lower_.size() == 0)
+      {
+        return v <= upper_;
+      }
+
+      if (upper_.size() == 0)
+      {
+        return v >= lower_;
+      }
+    
+      return (v >= lower_ && v <= upper_);
+    }
+  };
+
+
+  class DicomFindQuery::WildcardConstraint : public DicomFindQuery::IConstraint
+  {
+  private:
+    boost::regex pattern_;
+
+  public:
+    WildcardConstraint(const std::string& wildcard)
+    {
+      pattern_ = boost::regex(Toolbox::WildcardToRegularExpression(wildcard),
+                              boost::regex::icase /* case insensitive search */);
+    }
+
+    virtual bool Apply(const std::string& value) const
+    {
+      return boost::regex_match(value, pattern_);
+    }
+  };
+
+
+  void DicomFindQuery::PrepareMainDicomTags(ResourceType level)
+  {
+    std::set<DicomTag> tags;
+    DicomMap::GetMainDicomTags(tags, level);
+
+    for (std::set<DicomTag>::const_iterator
+           it = tags.begin(); it != tags.end(); ++it)
+    {
+      mainDicomTags_[*it] = level;
+    }
+  }
+
+
+  DicomFindQuery::DicomFindQuery() : 
+    level_(ResourceType_Patient),
+    filterJson_(false)
+  {
+    PrepareMainDicomTags(ResourceType_Patient);
+    PrepareMainDicomTags(ResourceType_Study);
+    PrepareMainDicomTags(ResourceType_Series);
+    PrepareMainDicomTags(ResourceType_Instance);
+  }
+
+
+  DicomFindQuery::~DicomFindQuery()
+  {
+    for (Constraints::iterator it = constraints_.begin();
+         it != constraints_.end(); it++)
+    {
+      delete it->second;
+    }
+  }
+
+
+
+
+  void DicomFindQuery::AssignConstraint(const DicomTag& tag,
+                                        IConstraint* constraint)
+  {
+    Constraints::iterator it = constraints_.find(tag);
+
+    if (it != constraints_.end())
+    {
+      constraints_.erase(it);
+    }
+
+    constraints_[tag] = constraint;
+
+    MainDicomTags::const_iterator tmp = mainDicomTags_.find(tag);
+    if (tmp == mainDicomTags_.end())
+    {
+      // The query depends upon a DICOM tag that is not a main tag
+      // from the point of view of Orthanc, we need to decode the
+      // JSON file on the disk.
+      filterJson_ = true;
+    }
+    else
+    {
+      filteredLevels_.insert(tmp->second);
+    }
+  }
+
+
+  void DicomFindQuery::SetConstraint(const DicomTag& tag,
+                                     const std::string& constraint)
+  {
+    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
+    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
+
+    if (constraint.find('-') != std::string::npos)
+    {
+      AssignConstraint(tag, new RangeConstraint(constraint));
+    }
+    else if (constraint.find('\\') != std::string::npos)
+    {
+      AssignConstraint(tag, new ListConstraint(constraint));
+    }
+    else if (constraint.find('*') != std::string::npos ||
+             constraint.find('?') != std::string::npos)
+    {
+      AssignConstraint(tag, new WildcardConstraint(constraint));
+    }
+    else
+    {
+      /**
+       * 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
+       *
+       * "Except for Attributes with a PN Value Representation, only
+       * entities with values which match exactly the value specified in the
+       * request shall match. This matching is case-sensitive, i.e.,
+       * sensitive to the exact encoding of the key attribute value in
+       * character sets where a letter may have multiple encodings (e.g.,
+       * based on its case, its position in a word, or whether it is
+       * accented)
+       * 
+       * For Attributes with a PN Value Representation (e.g., Patient Name
+       * (0010,0010)), an application may perform literal matching that is
+       * either case-sensitive, or that is insensitive to some or all
+       * aspects of case, position, accent, or other character encoding
+       * variants."
+       *
+       * (0008,0018) UI SOPInstanceUID     => Case-sensitive
+       * (0008,0050) SH AccessionNumber    => Case-sensitive
+       * (0010,0020) LO PatientID          => Case-sensitive
+       * (0020,000D) UI StudyInstanceUID   => Case-sensitive
+       * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
+      **/
+
+      AssignConstraint(tag, new ValueConstraint(constraint, FromDcmtkBridge::IsPNValueRepresentation(tag)));
+    }
+  }
+
+
+  bool DicomFindQuery::RestrictIdentifier(std::string& value,
+                                          DicomTag identifier) const
+  {
+    Constraints::const_iterator it = constraints_.find(identifier);
+    if (it == constraints_.end() ||
+        !it->second->IsExactConstraint())
+    {
+      return false;
+    }
+    else
+    {
+      value = dynamic_cast<ValueConstraint*>(it->second)->GetValue();
+      return true;
+    }
+  }
+
+  bool DicomFindQuery::HasMainDicomTagsFilter(ResourceType level) const
+  {
+    return filteredLevels_.find(level) != filteredLevels_.end();
+  }
+
+  bool DicomFindQuery::FilterMainDicomTags(const DicomMap& mainTags,
+                                           ResourceType level) const
+  {
+    std::set<DicomTag> tags;
+    mainTags.GetTags(tags);
+
+    for (std::set<DicomTag>::const_iterator
+           it = tags.begin(); it != tags.end(); ++it)
+    {
+      Constraints::const_iterator constraint = constraints_.find(*it);
+      if (!constraint->second->Apply(mainTags.GetValue(*it).AsString()))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  bool DicomFindQuery::HasInstanceFilter() const
+  {
+    return filterJson_;
+  }
+
+  bool DicomFindQuery::FilterInstance(const std::string& instanceId,
+                                      const Json::Value& content) const
+  {
+    for (Constraints::const_iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      std::string tag = it->first.Format();
+      std::string value;
+      if (content.isMember(tag))
+      {
+        value = content.get(tag, Json::arrayValue).get("Value", "").asString();
+      }
+
+      if (!it->second->Apply(value))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomFindQuery.h	Fri May 15 15:34:32 2015 +0200
@@ -0,0 +1,109 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ResourceFinder.h"
+
+namespace Orthanc
+{
+  class DicomFindQuery : public ResourceFinder::IQuery
+  {
+  private:
+    class  IConstraint : public boost::noncopyable
+    {
+    public:
+      virtual ~IConstraint()
+      {
+      }
+
+      virtual bool IsExactConstraint() const
+      {
+        return false;
+      }
+
+      virtual bool Apply(const std::string& value) const = 0;
+    };
+
+
+    class ValueConstraint;
+    class RangeConstraint;
+    class ListConstraint;
+    class WildcardConstraint;
+
+    typedef std::map<DicomTag, IConstraint*>  Constraints;
+    typedef std::map<DicomTag, ResourceType>  MainDicomTags;
+
+    MainDicomTags           mainDicomTags_;
+    ResourceType            level_;
+    bool                    filterJson_;
+    Constraints             constraints_;
+    std::set<ResourceType>  filteredLevels_;
+
+    void AssignConstraint(const DicomTag& tag,
+                          IConstraint* constraint);
+
+    void PrepareMainDicomTags(ResourceType level);
+
+
+  public:
+    DicomFindQuery();
+
+    virtual ~DicomFindQuery();
+
+    void SetLevel(ResourceType level)
+    {
+      level_ = level;
+    }
+
+    virtual ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetConstraint(const DicomTag& tag,
+                       const std::string& constraint);
+
+    virtual bool RestrictIdentifier(std::string& value,
+                                    DicomTag identifier) const;
+
+    virtual bool HasMainDicomTagsFilter(ResourceType level) const;
+
+    virtual bool FilterMainDicomTags(const DicomMap& mainTags,
+                                     ResourceType level) const;
+
+    virtual bool HasInstanceFilter() const;
+
+    virtual bool FilterInstance(const std::string& instanceId,
+                                const Json::Value& content) const;
+  };
+}
--- a/OrthancServer/FromDcmtkBridge.cpp	Fri May 15 13:17:37 2015 +0200
+++ b/OrthancServer/FromDcmtkBridge.cpp	Fri May 15 15:34:32 2015 +0200
@@ -721,4 +721,12 @@
       return false;
     }
   }
+
+
+  bool FromDcmtkBridge::IsPNValueRepresentation(const DicomTag& tag)
+  {
+    DcmTag t(tag.GetGroup(), tag.GetElement());
+    return t.getEVR() == EVR_PN;
+  }
+
 }
--- a/OrthancServer/FromDcmtkBridge.h	Fri May 15 13:17:37 2015 +0200
+++ b/OrthancServer/FromDcmtkBridge.h	Fri May 15 15:34:32 2015 +0200
@@ -105,5 +105,7 @@
 
     static bool SaveToMemoryBuffer(std::string& buffer,
                                    DcmDataset& dataSet);
+
+    static bool IsPNValueRepresentation(const DicomTag& tag);
   };
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri May 15 13:17:37 2015 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri May 15 15:34:32 2015 +0200
@@ -860,18 +860,18 @@
         request["Level"].type() == Json::stringValue &&
         request["Query"].type() == Json::objectValue)
     {
-      std::string level = request["Level"].asString();
-
-      ResourceFinder finder(context);
-      finder.SetLevel(StringToResourceType(level.c_str()));
-
       bool expand = false;
       if (request.isMember("Expand"))
       {
         expand = request["Expand"].asBool();
       }
 
-      /*if (request.isMember("CaseSensitive"))
+      std::string level = request["Level"].asString();
+
+      /*ResourceFinder finder(context);
+      finder.SetLevel(StringToResourceType(level.c_str()));
+
+      if (request.isMember("CaseSensitive"))
       {
         finder.SetCaseSensitive(request["CaseSensitive"].asBool());
       }
@@ -885,11 +885,11 @@
         }
 
         finder.AddTag(members[i], request["Query"][members[i]].asString());
-        }*/
+        }
 
       std::list<std::string> resources;
       finder.Apply(resources);
-      AnswerListOfResources(call.GetOutput(), context.GetIndex(), resources, finder.GetLevel(), expand);
+      AnswerListOfResources(call.GetOutput(), context.GetIndex(), resources, finder.GetLevel(), expand);*/
     }
     else
     {
--- a/OrthancServer/ResourceFinder.cpp	Fri May 15 13:17:37 2015 +0200
+++ b/OrthancServer/ResourceFinder.cpp	Fri May 15 15:34:32 2015 +0200
@@ -31,7 +31,7 @@
 
 
 #include "PrecompiledHeadersServer.h"
-#include "BaseResourceFinder.h"
+#include "ResourceFinder.h"
 
 #include "FromDcmtkBridge.h"
 #include "ServerContext.h"
@@ -41,12 +41,12 @@
 
 namespace Orthanc
 {
-  class BaseResourceFinder::CandidateResources
+  class ResourceFinder::CandidateResources
   {
   private:
     typedef std::map<DicomTag, std::string>  Query;
 
-    BaseResourceFinder&   finder_;
+    ResourceFinder&        finder_;
     ServerIndex&           index_;
     ResourceType           level_;
     bool                   isFilterApplied_;
@@ -64,49 +64,8 @@
     }
 
 
-    void RestrictIdentifier(const DicomTag& tag,
-                            const std::string& value)
-    {
-      assert((level_ == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) ||
-             (level_ == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) ||
-             (level_ == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
-             (level_ == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
-             (level_ == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
-
-      LOG(INFO) << "Lookup for identifier tag "
-                << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")";
-
-      std::list<std::string> resources;
-      index_.LookupIdentifier(resources, tag, value, level_);
-
-      if (isFilterApplied_)
-      {
-        std::set<std::string>  s;
-        ListToSet(s, resources);
-
-        std::set<std::string> tmp = filtered_;
-        filtered_.clear();
-
-        for (std::set<std::string>::const_iterator 
-               it = tmp.begin(); it != tmp.end(); ++it)
-        {
-          if (s.find(*it) != s.end())
-          {
-            filtered_.insert(*it);
-          }
-        }
-      }
-      else
-      {
-        assert(filtered_.empty());
-        isFilterApplied_ = true;
-        ListToSet(filtered_, resources);
-      }
-    }
-
-
   public:
-    CandidateResources(BaseResourceFinder& finder) : 
+    CandidateResources(ResourceFinder& finder) : 
       finder_(finder),
       index_(finder.context_.GetIndex()),
       level_(ResourceType_Patient), 
@@ -184,19 +143,56 @@
     }
 
     
-    void RestrictIdentifier(const DicomTag& tag)
+    void RestrictIdentifier(const IQuery& query,
+                            const DicomTag& tag)
     {
-      Identifiers::const_iterator it = finder_.identifiers_.find(tag);
-      if (it != finder_.identifiers_.end())
+      assert((level_ == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) ||
+             (level_ == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) ||
+             (level_ == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
+             (level_ == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
+             (level_ == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
+
+      std::string value;
+      if (!query.RestrictIdentifier(value, tag))
+      {
+        return;
+      }
+
+      LOG(INFO) << "Lookup for identifier tag "
+                << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")";
+
+      std::list<std::string> resources;
+      index_.LookupIdentifier(resources, tag, value, level_);
+
+      if (isFilterApplied_)
       {
-        RestrictIdentifier(it->first, it->second);
+        std::set<std::string>  s;
+        ListToSet(s, resources);
+
+        std::set<std::string> tmp = filtered_;
+        filtered_.clear();
+
+        for (std::set<std::string>::const_iterator 
+               it = tmp.begin(); it != tmp.end(); ++it)
+        {
+          if (s.find(*it) != s.end())
+          {
+            filtered_.insert(*it);
+          }
+        }
+      }
+      else
+      {
+        assert(filtered_.empty());
+        isFilterApplied_ = true;
+        ListToSet(filtered_, resources);
       }
     }
 
 
-    void RestrictMainDicomTags()
+    void RestrictMainDicomTags(const IQuery& query)
     {
-      if (finder_.mainTagsFilter_ == NULL)
+      if (!query.HasMainDicomTagsFilter(level_))
       {
         return;
       }
@@ -213,7 +209,7 @@
         DicomMap mainTags;
         if (index_.GetMainDicomTags(mainTags, *it, level_))
         {
-          if (finder_.mainTagsFilter_->Apply(mainTags, level_))
+          if (query.FilterMainDicomTags(mainTags, level_))
           {
             filtered_.insert(*it);
           }
@@ -223,46 +219,46 @@
   };
 
 
-  BaseResourceFinder::BaseResourceFinder(ServerContext& context) : 
+  ResourceFinder::ResourceFinder(ServerContext& context) : 
     context_(context),
-    level_(ResourceType_Patient),
     maxResults_(0)
   {
   }
 
 
-  void BaseResourceFinder::ApplyAtLevel(CandidateResources& candidates,
-                                        ResourceType level)
+  void ResourceFinder::ApplyAtLevel(CandidateResources& candidates,
+                                    const IQuery& query,
+                                    ResourceType level)
   {
     if (level != ResourceType_Patient)
     {
       candidates.GoDown();
     }
 
-    switch (level_)
+    switch (level)
     {
       case ResourceType_Patient:
       {
-        candidates.RestrictIdentifier(DICOM_TAG_PATIENT_ID);
+        candidates.RestrictIdentifier(query, DICOM_TAG_PATIENT_ID);
         break;
       }
 
       case ResourceType_Study:
       {
-        candidates.RestrictIdentifier(DICOM_TAG_STUDY_INSTANCE_UID);
-        candidates.RestrictIdentifier(DICOM_TAG_ACCESSION_NUMBER);
+        candidates.RestrictIdentifier(query, DICOM_TAG_STUDY_INSTANCE_UID);
+        candidates.RestrictIdentifier(query, DICOM_TAG_ACCESSION_NUMBER);
         break;
       }
 
       case ResourceType_Series:
       {
-        candidates.RestrictIdentifier(DICOM_TAG_SERIES_INSTANCE_UID);
+        candidates.RestrictIdentifier(query, DICOM_TAG_SERIES_INSTANCE_UID);
         break;
       }
 
       case ResourceType_Instance:
       {
-        candidates.RestrictIdentifier(DICOM_TAG_SOP_INSTANCE_UID);
+        candidates.RestrictIdentifier(query, DICOM_TAG_SOP_INSTANCE_UID);
         break;
       }
 
@@ -270,24 +266,11 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
-    candidates.RestrictMainDicomTags();
+    candidates.RestrictMainDicomTags(query);
   }
 
 
 
-  void BaseResourceFinder::SetIdentifier(const DicomTag& tag,
-                                         const std::string& value)
-  {
-    assert((level_ >= ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) ||
-           (level_ >= ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) ||
-           (level_ >= ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
-           (level_ >= ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
-           (level_ >= ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
-
-    identifiers_[tag] = value;
-  }
-
-
   static bool LookupOneInstance(std::string& result,
                                 ServerIndex& index,
                                 const std::string& id,
@@ -317,31 +300,34 @@
   }
 
 
-  bool BaseResourceFinder::Apply(std::list<std::string>& result)
+  bool ResourceFinder::Apply(std::list<std::string>& result,
+                             const IQuery& query)
   {
     CandidateResources candidates(*this);
 
-    ApplyAtLevel(candidates, ResourceType_Patient);
+    ApplyAtLevel(candidates, query, ResourceType_Patient);
+
+    const ResourceType level = query.GetLevel();
 
-    if (level_ == ResourceType_Study ||
-        level_ == ResourceType_Series ||
-        level_ == ResourceType_Instance)
+    if (level == ResourceType_Study ||
+        level == ResourceType_Series ||
+        level == ResourceType_Instance)
     {
-      ApplyAtLevel(candidates, ResourceType_Study);
+      ApplyAtLevel(candidates, query, ResourceType_Study);
     }
         
-    if (level_ == ResourceType_Series ||
-        level_ == ResourceType_Instance)
+    if (level == ResourceType_Series ||
+        level == ResourceType_Instance)
     {
-      ApplyAtLevel(candidates, ResourceType_Series);
+      ApplyAtLevel(candidates, query, ResourceType_Series);
     }
         
-    if (level_ == ResourceType_Instance)
+    if (level == ResourceType_Instance)
     {
-      ApplyAtLevel(candidates, ResourceType_Instance);
+      ApplyAtLevel(candidates, query, ResourceType_Instance);
     }
 
-    if (instanceFilter_ == NULL)
+    if (!query.HasInstanceFilter())
     {
       candidates.Flatten(result);
 
@@ -368,11 +354,11 @@
         try
         {
           std::string instance;
-          if (LookupOneInstance(instance, context_.GetIndex(), *resource, level_))
+          if (LookupOneInstance(instance, context_.GetIndex(), *resource, level))
           {
             Json::Value content;
             context_.ReadJson(content, instance);
-            if (instanceFilter_->Apply(*resource, content))
+            if (query.FilterInstance(*resource, content))
             {
               result.push_back(*resource);
 
--- a/OrthancServer/ResourceFinder.h	Fri May 15 13:17:37 2015 +0200
+++ b/OrthancServer/ResourceFinder.h	Fri May 15 15:34:32 2015 +0200
@@ -38,30 +38,30 @@
 
 namespace Orthanc
 {
-  class BaseResourceFinder : public boost::noncopyable
+  class ResourceFinder : public boost::noncopyable
   {
   public:
-    class IMainTagsFilter : public boost::noncopyable
+    class IQuery : public boost::noncopyable
     {
     public:
-      virtual ~IMainTagsFilter()
+      virtual ~IQuery()
       {
       }
 
-      virtual bool Apply(const DicomMap& mainTags,
-                         ResourceType level) = 0;
-    };
+      virtual ResourceType GetLevel() const = 0;
 
+      virtual bool RestrictIdentifier(std::string& value,
+                                      DicomTag identifier) const = 0;
+
+      virtual bool HasMainDicomTagsFilter(ResourceType level) const = 0;
 
-    class IInstanceFilter : public boost::noncopyable
-    {
-    public:
-      virtual ~IInstanceFilter()
-      {
-      }
+      virtual bool FilterMainDicomTags(const DicomMap& mainTags,
+                                       ResourceType level) const = 0;
 
-      virtual bool Apply(const std::string& instanceId,
-                         const Json::Value& content) = 0;
+      virtual bool HasInstanceFilter() const = 0;
+
+      virtual bool FilterInstance(const std::string& instanceId,
+                                  const Json::Value& content) const = 0;
     };
 
 
@@ -71,40 +71,14 @@
     class CandidateResources;
 
     ServerContext&    context_;
-    ResourceType      level_;
     size_t            maxResults_;
-    Identifiers       identifiers_;
-    IMainTagsFilter  *mainTagsFilter_;
-    IInstanceFilter  *instanceFilter_;
 
     void ApplyAtLevel(CandidateResources& candidates,
+                      const IQuery& query,
                       ResourceType level);
 
   public:
-    BaseResourceFinder(ServerContext& context);
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    void SetLevel(ResourceType level)
-    {
-      level_ = level;
-    }
-
-    void SetIdentifier(const DicomTag& tag,
-                       const std::string& value);
-
-    void SetMainTagsFilter(IMainTagsFilter& filter)
-    {
-      mainTagsFilter_ = &filter;
-    }
-
-    void SetInstanceFilter(IInstanceFilter& filter)
-    {
-      instanceFilter_ = &filter;
-    }
+    ResourceFinder(ServerContext& context);
 
     void SetMaxResults(size_t value)
     {
@@ -119,7 +93,8 @@
     // Returns "true" iff. all the matching resources have been
     // returned. Will be "false" if the results were truncated by
     // "SetMaxResults()".
-    bool Apply(std::list<std::string>& result);
+    bool Apply(std::list<std::string>& result,
+               const IQuery& query);
   };
 
 }
--- a/UnitTestsSources/DicomMapTests.cpp	Fri May 15 13:17:37 2015 +0200
+++ b/UnitTestsSources/DicomMapTests.cpp	Fri May 15 15:34:32 2015 +0200
@@ -83,22 +83,38 @@
 
 TEST(DicomMap, Tags)
 {
+  std::set<DicomTag> s;
+
   DicomMap m;
+  m.GetTags(s);
+  ASSERT_EQ(0, s.size());
+
   ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_NAME));
   ASSERT_FALSE(m.HasTag(0x0010, 0x0010));
   m.SetValue(0x0010, 0x0010, "PatientName");
   ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_NAME));
   ASSERT_TRUE(m.HasTag(0x0010, 0x0010));
 
+  m.GetTags(s);
+  ASSERT_EQ(1, s.size());
+  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin());
+
   ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_ID));
   m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID");
   ASSERT_TRUE(m.HasTag(0x0010, 0x0020));
   m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2");
   ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).AsString());
 
+  m.GetTags(s);
+  ASSERT_EQ(2, s.size());
+
   m.Remove(DICOM_TAG_PATIENT_ID);
   ASSERT_THROW(m.GetValue(0x0010, 0x0020), OrthancException);
 
+  m.GetTags(s);
+  ASSERT_EQ(1, s.size());
+  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin());
+
   std::auto_ptr<DicomMap> mm(m.Clone());
   ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).AsString());  
 
--- a/UnitTestsSources/FromDcmtkTests.cpp	Fri May 15 13:17:37 2015 +0200
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Fri May 15 15:34:32 2015 +0200
@@ -287,3 +287,10 @@
     }
   }
 }
+
+
+TEST(FromDcmtkBridge, VR)
+{
+  ASSERT_TRUE(FromDcmtkBridge::IsPNValueRepresentation(DICOM_TAG_PATIENT_NAME));
+  ASSERT_FALSE(FromDcmtkBridge::IsPNValueRepresentation(DICOM_TAG_PATIENT_ID));
+}