changeset 1763:f7014cca73c7

integration db-changes->mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 29 Oct 2015 12:45:20 +0100
parents 8fc1d096aa38 (current diff) 2b91363cc1d1 (diff)
children 9ead18ef460a
files OrthancServer/DicomFindQuery.cpp OrthancServer/DicomFindQuery.h OrthancServer/ResourceFinder.cpp OrthancServer/ResourceFinder.h
diffstat 60 files changed, 2507 insertions(+), 1333 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Fri Oct 23 17:04:22 2015 +0200
+++ b/CMakeLists.txt	Thu Oct 29 12:45:20 2015 +0100
@@ -150,7 +150,6 @@
   OrthancServer/DatabaseWrapper.cpp
   OrthancServer/DatabaseWrapperBase.cpp
   OrthancServer/DicomDirWriter.cpp
-  OrthancServer/DicomFindQuery.cpp
   OrthancServer/DicomModification.cpp
   OrthancServer/DicomProtocol/DicomFindAnswers.cpp
   OrthancServer/DicomProtocol/DicomServer.cpp
@@ -179,7 +178,13 @@
   OrthancServer/OrthancRestApi/OrthancRestSystem.cpp
   OrthancServer/ParsedDicomFile.cpp
   OrthancServer/QueryRetrieveHandler.cpp
-  OrthancServer/ResourceFinder.cpp
+  OrthancServer/Search/LookupIdentifierQuery.cpp
+  OrthancServer/Search/LookupResource.cpp
+  OrthancServer/Search/SetOfResources.cpp
+  OrthancServer/Search/ListConstraint.cpp
+  OrthancServer/Search/RangeConstraint.cpp
+  OrthancServer/Search/ValueConstraint.cpp
+  OrthancServer/Search/WildcardConstraint.cpp
   OrthancServer/ServerContext.cpp
   OrthancServer/ServerEnumerations.cpp
   OrthancServer/ServerIndex.cpp
--- a/Core/DicomFormat/DicomMap.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/Core/DicomFormat/DicomMap.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -69,7 +69,7 @@
     //DicomTag(0x0010, 0x1080), // MilitaryRank
     DicomTag(0x0008, 0x0021),   // SeriesDate
     DicomTag(0x0008, 0x0031),   // SeriesTime
-    DicomTag(0x0008, 0x0060),   // Modality
+    DICOM_TAG_MODALITY,
     DicomTag(0x0008, 0x0070),   // Manufacturer
     DicomTag(0x0008, 0x1010),   // StationName
     DICOM_TAG_SERIES_DESCRIPTION,
@@ -100,6 +100,36 @@
   };
 
 
+  void DicomMap::LoadMainDicomTags(const DicomTag*& tags,
+                                   size_t& size,
+                                   ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        tags = patientTags;
+        size = sizeof(patientTags) / sizeof(DicomTag);
+        break;
+
+      case ResourceType_Study:
+        tags = studyTags;
+        size = sizeof(studyTags) / sizeof(DicomTag);
+        break;
+
+      case ResourceType_Series:
+        tags = seriesTags;
+        size = sizeof(seriesTags) / sizeof(DicomTag);
+        break;
+
+      case ResourceType_Instance:
+        tags = instanceTags;
+        size = sizeof(instanceTags) / sizeof(DicomTag);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
 
 
   void DicomMap::SetValue(uint16_t group, 
--- a/Core/DicomFormat/DicomMap.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Core/DicomFormat/DicomMap.h	Thu Oct 29 12:45:20 2015 +0100
@@ -172,5 +172,9 @@
     void Print(FILE* fp) const;
 
     void GetTags(std::set<DicomTag>& tags) const;
+
+    static void LoadMainDicomTags(const DicomTag*& tags,
+                                  size_t& size,
+                                  ResourceType level);
   };
 }
--- a/Core/DicomFormat/DicomTag.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Core/DicomFormat/DicomTag.h	Thu Oct 29 12:45:20 2015 +0100
@@ -109,6 +109,7 @@
 
   static const DicomTag DICOM_TAG_STUDY_DESCRIPTION(0x0008, 0x1030);
   static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e);
+  static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060);
 
   // The following is used for "modify/anonymize" operations
   static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016);
--- a/Core/DicomFormat/DicomValue.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/Core/DicomFormat/DicomValue.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -81,10 +81,13 @@
   }
 
   
+#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1
   void DicomValue::FormatDataUriScheme(std::string& target,
                                        const std::string& mime) const
   {
     Toolbox::EncodeBase64(target, GetContent());
     target.insert(0, "data:" + mime + ";base64,");
   }
+#endif
+
 }
--- a/Core/DicomFormat/DicomValue.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Core/DicomFormat/DicomValue.h	Thu Oct 29 12:45:20 2015 +0100
@@ -78,6 +78,7 @@
     
     DicomValue* Clone() const;
 
+#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1
     void FormatDataUriScheme(std::string& target,
                              const std::string& mime) const;
 
@@ -85,5 +86,6 @@
     {
       FormatDataUriScheme(target, "application/octet-stream");
     }
+#endif
   };
 }
--- a/Core/Enumerations.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/Core/Enumerations.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -148,6 +148,9 @@
       case ErrorCode_StorageAreaPlugin:
         return "Error in the plugin implementing a custom storage area";
 
+      case ErrorCode_EmptyRequest:
+        return "The request is empty";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
--- a/Core/Enumerations.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Core/Enumerations.h	Thu Oct 29 12:45:20 2015 +0100
@@ -79,6 +79,7 @@
     ErrorCode_BadFont = 30    /*!< Badly formatted font file */,
     ErrorCode_DatabasePlugin = 31    /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */,
     ErrorCode_StorageAreaPlugin = 32    /*!< Error in the plugin implementing a custom storage area */,
+    ErrorCode_EmptyRequest = 33    /*!< The request is empty */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
--- a/Core/FileStorage/StorageAccessor.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/Core/FileStorage/StorageAccessor.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -126,6 +126,20 @@
   }
 
 
+  void StorageAccessor::Read(Json::Value& content,
+                             const FileInfo& info)
+  {
+    std::string s;
+    Read(s, info);
+
+    Json::Reader reader;
+    if (!reader.parse(s, content))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
   void StorageAccessor::SetupSender(BufferHttpSender& sender,
                                     const FileInfo& info)
   {
--- a/Core/FileStorage/StorageAccessor.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Core/FileStorage/StorageAccessor.h	Thu Oct 29 12:45:20 2015 +0100
@@ -41,6 +41,7 @@
 #include <string>
 #include <boost/noncopyable.hpp>
 #include <stdint.h>
+#include <json/value.h>
 
 namespace Orthanc
 {
@@ -75,6 +76,9 @@
     void Read(std::string& content,
               const FileInfo& info);
 
+    void Read(Json::Value& content,
+              const FileInfo& info);
+
     void Remove(const FileInfo& info)
     {
       area_.Remove(info.GetUuid(), info.GetContentType());
--- a/NEWS	Fri Oct 23 17:04:22 2015 +0200
+++ b/NEWS	Thu Oct 29 12:45:20 2015 +0100
@@ -1,6 +1,7 @@
 Pending changes in the mainline
 ===============================
 
+* Full indexation of the patient/study tags to speed up searches and C-Find
 * Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive")
 * "/tools/create-dicom": Support of binary tags encoded using data URI scheme
 * "/tools/create-dicom": Support of hierarchical structures (creation of sequences)
@@ -24,6 +25,7 @@
 Maintenance
 -----------
 
+* Full refactoring of the searching features
 * C-Move SCP for studies using AccessionNumber tag
 * Fix issue 4 (C-Store Association not renegotiated on Specific-to-specific transfer syntax change)
 * "/system" URI gives information about the plugins used for storage area and DB back-end
--- a/OrthancServer/DatabaseWrapper.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/DatabaseWrapper.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -373,7 +373,7 @@
                   boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";");
       db_.CommitTransaction();
       version_ = 6;
-    }    
+    }
   }
 
 
--- a/OrthancServer/DatabaseWrapper.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/DatabaseWrapper.h	Thu Oct 29 12:45:20 2015 +0100
@@ -249,6 +249,12 @@
       return base_.GetResourceCount(resourceType);
     }
 
+    virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                   ResourceType resourceType)
+    {
+      base_.GetAllInternalIds(target, resourceType);
+    }
+
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType)
     {
@@ -315,11 +321,13 @@
       return base_.IsExistingResource(internalId);
     }
 
-    virtual void LookupIdentifier(std::list<int64_t>& target,
+    virtual void LookupIdentifier(std::list<int64_t>& result,
+                                  ResourceType level,
                                   const DicomTag& tag,
+                                  IdentifierConstraintType type,
                                   const std::string& value)
     {
-      base_.LookupIdentifier(target, tag, value);
+      base_.LookupIdentifier(result, level, tag, type, value);
     }
 
     virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
--- a/OrthancServer/DatabaseWrapperBase.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/DatabaseWrapperBase.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -34,6 +34,7 @@
 #include "DatabaseWrapperBase.h"
 
 #include <stdio.h>
+#include <memory>
 
 namespace Orthanc
 {
@@ -532,6 +533,20 @@
     return static_cast<uint64_t>(s.ColumnInt64(0));
   }
 
+  void DatabaseWrapperBase::GetAllInternalIds(std::list<int64_t>& target,
+                                              ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnInt64(0));
+    }
+  }
+
+
   void DatabaseWrapperBase::GetAllPublicIds(std::list<std::string>& target,
                                             ResourceType resourceType)
   {
@@ -665,28 +680,51 @@
   }
 
 
+
   void DatabaseWrapperBase::LookupIdentifier(std::list<int64_t>& target,
+                                             ResourceType level,
                                              const DicomTag& tag,
+                                             IdentifierConstraintType type,
                                              const std::string& value)
   {
-    assert(tag == DICOM_TAG_PATIENT_ID ||
-           tag == DICOM_TAG_STUDY_INSTANCE_UID ||
-           tag == DICOM_TAG_SERIES_INSTANCE_UID ||
-           tag == DICOM_TAG_SOP_INSTANCE_UID ||
-           tag == DICOM_TAG_ACCESSION_NUMBER);
-    
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT id FROM DicomIdentifiers WHERE tagGroup=? AND tagElement=? and value=?");
+    static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
+                                 "d.id = r.internalId AND r.resourceType=? AND "
+                                 "d.tagGroup=? AND d.tagElement=? AND ");
+
+    std::auto_ptr<SQLite::Statement> s;
+
+    switch (type)
+    {
+      case IdentifierConstraintType_GreaterOrEqual:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?"));
+        break;
+
+      case IdentifierConstraintType_SmallerOrEqual:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?"));
+        break;
 
-    s.BindInt(0, tag.GetGroup());
-    s.BindInt(1, tag.GetElement());
-    s.BindString(2, value);
+      case IdentifierConstraintType_Wildcard:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?"));
+        break;
+
+      case IdentifierConstraintType_Equal:
+      default:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?"));
+        break;
+    }
+
+    assert(s.get() != NULL);
+
+    s->BindInt(0, level);
+    s->BindInt(1, tag.GetGroup());
+    s->BindInt(2, tag.GetElement());
+    s->BindString(3, value);
 
     target.clear();
 
-    while (s.Step())
+    while (s->Step())
     {
-      target.push_back(s.ColumnInt64(0));
-    }
+      target.push_back(s->ColumnInt64(0));
+    }    
   }
 }
--- a/OrthancServer/DatabaseWrapperBase.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/DatabaseWrapperBase.h	Thu Oct 29 12:45:20 2015 +0100
@@ -168,6 +168,9 @@
     
     uint64_t GetTotalUncompressedSize();
 
+    void GetAllInternalIds(std::list<int64_t>& target,
+                           ResourceType resourceType);
+
     void GetAllPublicIds(std::list<std::string>& target,
                          ResourceType resourceType);
 
@@ -190,8 +193,10 @@
 
     bool IsExistingResource(int64_t internalId);
 
-    void LookupIdentifier(std::list<int64_t>& target,
+    void LookupIdentifier(std::list<int64_t>& result,
+                          ResourceType level,
                           const DicomTag& tag,
+                          IdentifierConstraintType type,
                           const std::string& value);
   };
 }
--- a/OrthancServer/DicomFindQuery.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,395 +0,0 @@
-/**
- * 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,
-                       bool caseSensitive)
-    {
-      std::string re = Toolbox::WildcardToRegularExpression(wildcard);
-
-      if (caseSensitive)
-      {
-        pattern_ = boost::regex(re);
-      }
-      else
-      {
-        pattern_ = boost::regex(re, 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,
-                                     bool caseSensitivePN)
-  {
-    ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag);
-
-    bool sensitive = true;
-    if (vr == ValueRepresentation_PatientName)
-    {
-      sensitive = caseSensitivePN;
-    }
-
-    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
-    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
-
-    if ((vr == ValueRepresentation_Date ||
-         vr == ValueRepresentation_DateTime ||
-         vr == ValueRepresentation_Time) &&
-        constraint.find('-') != std::string::npos)
-    {
-      /**
-       * Range matching is only defined for TM, DA and DT value
-       * representations. This code fixes issues 35 and 37.
-       *
-       * Reference: "Range matching is not defined for types of
-       * Attributes other than dates and times", DICOM PS 3.4,
-       * C.2.2.2.5 ("Range Matching").
-       **/
-      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, sensitive));
-    }
-    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, sensitive));
-    }
-  }
-
-
-  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 std::string& resourceId,
-                                           ResourceType level,
-                                           const DicomMap& mainTags) const
-  {
-    std::set<DicomTag> tags;
-    mainTags.GetTags(tags);
-
-    for (std::set<DicomTag>::const_iterator
-           it = tags.begin(); it != tags.end(); ++it)
-    {
-      const DicomValue& value = mainTags.GetValue(*it);
-      if (value.IsBinary() || value.IsNull())
-      {
-        return false;
-      }
-
-      Constraints::const_iterator constraint = constraints_.find(*it);
-      if (constraint != constraints_.end() &&
-          !constraint->second->Apply(value.GetContent()))
-      {
-        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;
-  }
-}
--- a/OrthancServer/DicomFindQuery.h	Fri Oct 23 17:04:22 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-/**
- * 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,
-                       bool caseSensitivePN);
-
-    virtual bool RestrictIdentifier(std::string& value,
-                                    DicomTag identifier) const;
-
-    virtual bool HasMainDicomTagsFilter(ResourceType level) const;
-
-    virtual bool FilterMainDicomTags(const std::string& resourceId,
-                                     ResourceType level,
-                                     const DicomMap& mainTags) const;
-
-    virtual bool HasInstanceFilter() const;
-
-    virtual bool FilterInstance(const std::string& instanceId,
-                                const Json::Value& content) const;
-  };
-}
--- a/OrthancServer/IDatabaseWrapper.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/IDatabaseWrapper.h	Thu Oct 29 12:45:20 2015 +0100
@@ -83,6 +83,9 @@
     virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
                                 int64_t id) = 0;
 
+    virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                   ResourceType resourceType) = 0;
+
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType) = 0;
 
@@ -146,8 +149,10 @@
     virtual bool LookupGlobalProperty(std::string& target,
                                       GlobalProperty property) = 0;
 
-    virtual void LookupIdentifier(std::list<int64_t>& target,
+    virtual void LookupIdentifier(std::list<int64_t>& result,
+                                  ResourceType level,
                                   const DicomTag& tag,
+                                  IdentifierConstraintType type,
                                   const std::string& value) = 0;
 
     virtual bool LookupMetadata(std::string& target,
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -33,14 +33,12 @@
 #include "PrecompiledHeadersServer.h"
 #include "OrthancFindRequestHandler.h"
 
+#include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/Logging.h"
-#include "../Core/DicomFormat/DicomArray.h"
-#include "ServerToolbox.h"
+#include "FromDcmtkBridge.h"
 #include "OrthancInitialization.h"
-#include "FromDcmtkBridge.h"
-
-#include "ResourceFinder.h"
-#include "DicomFindQuery.h"
+#include "Search/LookupResource.h"
+#include "ServerToolbox.h"
 
 #include <boost/regex.hpp> 
 
@@ -90,130 +88,6 @@
   }
 
 
-  namespace
-  {
-    class CFindQuery : public DicomFindQuery
-    {
-    private:
-      DicomFindAnswers&      answers_;
-      ServerIndex&           index_;
-      const DicomArray&      query_;
-      bool                   hasModalitiesInStudy_;
-      std::set<std::string>  modalitiesInStudy_;
-
-    public:
-      CFindQuery(DicomFindAnswers& answers,
-                 ServerIndex& index,
-                 const DicomArray& query) :
-        answers_(answers),
-        index_(index),
-        query_(query),
-        hasModalitiesInStudy_(false)
-      {
-      }
-
-      void SetModalitiesInStudy(const std::string& value)
-      {
-        hasModalitiesInStudy_ = true;
-        
-        std::vector<std::string>  tmp;
-        Toolbox::TokenizeString(tmp, value, '\\'); 
-
-        for (size_t i = 0; i < tmp.size(); i++)
-        {
-          modalitiesInStudy_.insert(tmp[i]);
-        }
-      }
-
-      virtual bool HasMainDicomTagsFilter(ResourceType level) const
-      {
-        if (DicomFindQuery::HasMainDicomTagsFilter(level))
-        {
-          return true;
-        }
-
-        return (level == ResourceType_Study &&
-                hasModalitiesInStudy_);
-      }
-
-      virtual bool FilterMainDicomTags(const std::string& resourceId,
-                                       ResourceType level,
-                                       const DicomMap& mainTags) const
-      {
-        if (!DicomFindQuery::FilterMainDicomTags(resourceId, level, mainTags))
-        {
-          return false;
-        }
-
-        if (level != ResourceType_Study ||
-            !hasModalitiesInStudy_)
-        {
-          return true;
-        }
-
-        try
-        {
-          // We are considering a single study, and the
-          // "MODALITIES_IN_STUDY" tag is set in the C-Find. Check
-          // whether one of its child series matches one of the
-          // modalities.
-
-          Json::Value study;
-          if (index_.LookupResource(study, resourceId, ResourceType_Study))
-          {
-            // Loop over the series of the considered study.
-            for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++)
-            {
-              Json::Value series;
-              if (index_.LookupResource(series, study["Series"][j].asString(), ResourceType_Series))
-              {
-                // Get the modality of this series
-                if (series["MainDicomTags"].isMember("Modality"))
-                {
-                  std::string modality = series["MainDicomTags"]["Modality"].asString();
-                  if (modalitiesInStudy_.find(modality) != modalitiesInStudy_.end())
-                  {
-                    // This series of the considered study matches one
-                    // of the required modalities. Take the study into
-                    // consideration for future filtering.
-                    return true;
-                  }
-                }
-              }
-            }
-          }
-        }
-        catch (OrthancException&)
-        {
-          // This resource has probably been deleted during the find request
-        }
-
-        return false;
-      }
-
-      virtual bool HasInstanceFilter() const
-      {
-        return true;
-      }
-
-      virtual bool FilterInstance(const std::string& instanceId,
-                                  const Json::Value& content) const
-      {
-        bool ok = DicomFindQuery::FilterInstance(instanceId, content);
-
-        if (ok)
-        {
-          // Add this resource to the answers
-          AddAnswer(answers_, content, query_);
-        }
-
-        return ok;
-      }
-    };
-  }
-
-
-
   bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
                                          const DicomMap& input,
                                          const std::string& remoteIp,
@@ -276,9 +150,8 @@
      * Build up the query object.
      **/
 
-    CFindQuery findQuery(answers, context_.GetIndex(), query);
-    findQuery.SetLevel(level);
-        
+    LookupResource finder(level);
+
     for (size_t i = 0; i < query.GetSize(); i++)
     {
       const DicomTag tag = query.GetElement(i).GetTag();
@@ -297,14 +170,17 @@
         continue;
       }
 
-      if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
+      ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag);
+
+      // DICOM specifies that searches must be case sensitive, except
+      // for tags with a PN value representation
+      bool sensitive = true;
+      if (vr == ValueRepresentation_PatientName)
       {
-        findQuery.SetModalitiesInStudy(value);
+        sensitive = caseSensitivePN;
       }
-      else
-      {
-        findQuery.SetConstraint(tag, value, caseSensitivePN);
-      }
+
+      finder.AddDicomConstraint(tag, value, sensitive);
     }
 
 
@@ -312,28 +188,35 @@
      * Run the query.
      **/
 
-    ResourceFinder finder(context_);
+    size_t maxResults = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;
+
+    std::vector<std::string> resources, instances;
+    context_.GetIndex().FindCandidates(resources, instances, finder);
 
-    switch (level)
+    assert(resources.size() == instances.size());
+    bool finished = true;
+
+    for (size_t i = 0; i < instances.size(); i++)
     {
-      case ResourceType_Patient:
-      case ResourceType_Study:
-      case ResourceType_Series:
-        finder.SetMaxResults(maxResults_);
-        break;
-
-      case ResourceType_Instance:
-        finder.SetMaxResults(maxInstances_);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
+      Json::Value dicom;
+      context_.ReadJson(dicom, instances[i]);
+      
+      if (finder.IsMatch(dicom))
+      {
+        if (maxResults != 0 &&
+            answers.GetSize() >= maxResults)
+        {
+          finished = false;
+          break;
+        }
+        else
+        {
+          AddAnswer(answers, dicom, query);
+        }
+      }
     }
 
-    std::list<std::string> tmp;
-    bool finished = finder.Apply(tmp, findQuery);
-
-    LOG(INFO) << "Number of matching resources: " << tmp.size();
+    LOG(INFO) << "Number of matching resources: " << answers.GetSize();
 
     return finished;
   }
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -148,7 +148,7 @@
     const std::string& content = value.GetContent();
 
     std::list<std::string> ids;
-    context_.GetIndex().LookupIdentifier(ids, tag, content, level);
+    context_.GetIndex().LookupIdentifierExact(ids, level, tag, content);
 
     if (ids.size() != 1)
     {
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -36,8 +36,6 @@
 #include "../../Core/Logging.h"
 #include "../ServerToolbox.h"
 #include "../FromDcmtkBridge.h"
-#include "../ResourceFinder.h"
-#include "../DicomFindQuery.h"
 #include "../ServerContext.h"
 #include "../SliceOrdering.h"
 
@@ -892,7 +890,7 @@
                                       ResourceType level)
   {
     std::list<std::string> tmp;
-    index.LookupIdentifier(tmp, tag, value, level);
+    index.LookupIdentifierExact(tmp, level, tag, value);
 
     for (std::list<std::string>::const_iterator
            it = tmp.begin(); it != tmp.end(); ++it)
@@ -944,7 +942,8 @@
         request.isMember("Query") &&
         request["Level"].type() == Json::stringValue &&
         request["Query"].type() == Json::objectValue &&
-        (!request.isMember("CaseSensitive") || request["CaseSensitive"].type() == Json::booleanValue))
+        (!request.isMember("CaseSensitive") || request["CaseSensitive"].type() == Json::booleanValue) &&
+        (!request.isMember("Limit") || request["Limit"].type() == Json::intValue))
     {
       bool expand = false;
       if (request.isMember("Expand"))
@@ -958,10 +957,19 @@
         caseSensitive = request["CaseSensitive"].asBool();
       }
 
+      size_t limit = 0;
+      if (request.isMember("Limit"))
+      {
+        limit = request["CaseSensitive"].asInt();
+        if (limit < 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+
       std::string level = request["Level"].asString();
 
-      DicomFindQuery query;
-      query.SetLevel(StringToResourceType(level.c_str()));
+      LookupResource query(StringToResourceType(level.c_str()));
 
       Json::Value::Members members = request["Query"].getMemberNames();
       for (size_t i = 0; i < members.size(); i++)
@@ -971,14 +979,13 @@
           throw OrthancException(ErrorCode_BadRequest);
         }
 
-        query.SetConstraint(FromDcmtkBridge::ParseTag(members[i]), 
-                            request["Query"][members[i]].asString(),
-                            caseSensitive);
+        query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), 
+                                 request["Query"][members[i]].asString(),
+                                 caseSensitive);
       }
       
       std::list<std::string> resources;
-      ResourceFinder finder(context);
-      finder.Apply(resources, query);
+      context.Apply(resources, query, limit);
       AnswerListOfResources(call.GetOutput(), context.GetIndex(), resources, query.GetLevel(), expand);
     }
     else
--- a/OrthancServer/ResourceFinder.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,409 +0,0 @@
-/**
- * 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 "ResourceFinder.h"
-
-#include "../Core/Logging.h"
-#include "FromDcmtkBridge.h"
-#include "ServerContext.h"
-
-namespace Orthanc
-{
-  class ResourceFinder::CandidateResources
-  {
-  private:
-    typedef std::map<DicomTag, std::string>  Query;
-
-    ResourceFinder&        finder_;
-    ServerIndex&           index_;
-    ResourceType           level_;
-    bool                   isFilterApplied_;
-    std::set<std::string>  filtered_;
-
-     
-    static void ListToSet(std::set<std::string>& target,
-                          const std::list<std::string>& source)
-    {
-      for (std::list<std::string>::const_iterator
-             it = source.begin(); it != source.end(); ++it)
-      {
-        target.insert(*it);
-      }
-    }
-
-
-  public:
-    CandidateResources(ResourceFinder& finder) : 
-      finder_(finder),
-      index_(finder.context_.GetIndex()),
-      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;
-          try
-          {
-            index_.GetChildren(children, *it);
-            ListToSet(filtered_, children);
-          }
-          catch (OrthancException&)
-          {
-            // The resource was removed in the meantime
-          }
-        }
-      }
-
-      switch (level_)
-      {
-        case ResourceType_Patient:
-          level_ = ResourceType_Study;
-          break;
-
-        case ResourceType_Study:
-          level_ = ResourceType_Series;
-          break;
-
-        case ResourceType_Series:
-          level_ = ResourceType_Instance;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-
-    void Flatten(std::list<std::string>& resources) const
-    {
-      resources.clear();
-
-      if (isFilterApplied_)
-      {
-        for (std::set<std::string>::const_iterator 
-               it = filtered_.begin(); it != filtered_.end(); ++it)
-        {
-          resources.push_back(*it);
-        }
-      }
-      else
-      {
-        index_.GetAllUuids(resources, level_);
-      }
-    }
-
-    
-    void RestrictIdentifier(const IQuery& query,
-                            const DicomTag& tag)
-    {
-      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_)
-      {
-        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(const IQuery& query,
-                               bool filterPatientTagsAtStudyLevel)
-    {
-      if (filterPatientTagsAtStudyLevel &&
-          level_ == ResourceType_Patient)
-      {
-        return;
-      }
-
-      bool hasTagsAtThisLevel = query.HasMainDicomTagsFilter(level_);
-      bool hasTagsAtPatientLevel = (filterPatientTagsAtStudyLevel &&
-                                    level_ == ResourceType_Study &&
-                                    query.HasMainDicomTagsFilter(ResourceType_Patient));
-
-      if (!hasTagsAtThisLevel && !hasTagsAtPatientLevel)        
-      {
-        return;
-      }
-
-      std::list<std::string> resources;
-      Flatten(resources);
-
-      isFilterApplied_ = true;
-      filtered_.clear();
-
-      for (std::list<std::string>::const_iterator
-             it = resources.begin(); it != resources.end(); ++it)
-      {
-        DicomMap mainTags;
-
-        if (hasTagsAtThisLevel &&
-            (!index_.GetMainDicomTags(mainTags, *it, level_, level_) ||
-             !query.FilterMainDicomTags(*it, level_, mainTags)))
-        {
-          continue;
-        }
-
-        if (hasTagsAtPatientLevel &&
-            (!index_.GetMainDicomTags(mainTags, *it, ResourceType_Study, ResourceType_Patient) ||
-             !query.FilterMainDicomTags(*it, ResourceType_Patient, mainTags)))
-        {
-          continue;
-        }
-
-        filtered_.insert(*it);
-      }
-    }
-  };
-
-
-  ResourceFinder::ResourceFinder(ServerContext& context) : 
-    context_(context),
-    maxResults_(0)
-  {
-  }
-
-
-  void ResourceFinder::ApplyAtLevel(CandidateResources& candidates,
-                                    const IQuery& query,
-                                    ResourceType level)
-  {
-    if (level != ResourceType_Patient)
-    {
-      candidates.GoDown();
-    }
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-      {
-        candidates.RestrictIdentifier(query, DICOM_TAG_PATIENT_ID);
-        break;
-      }
-
-      case ResourceType_Study:
-      {
-        candidates.RestrictIdentifier(query, DICOM_TAG_STUDY_INSTANCE_UID);
-        candidates.RestrictIdentifier(query, DICOM_TAG_ACCESSION_NUMBER);
-        break;
-      }
-
-      case ResourceType_Series:
-      {
-        candidates.RestrictIdentifier(query, DICOM_TAG_SERIES_INSTANCE_UID);
-        break;
-      }
-
-      case ResourceType_Instance:
-      {
-        candidates.RestrictIdentifier(query, DICOM_TAG_SOP_INSTANCE_UID);
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (query.GetLevel() == ResourceType_Patient)
-    {
-      candidates.RestrictMainDicomTags(query, false);
-    }
-    else
-    {
-      candidates.RestrictMainDicomTags(query, true);
-    }
-  }
-
-
-
-  static bool LookupOneInstance(std::string& result,
-                                ServerIndex& index,
-                                const std::string& id,
-                                ResourceType type)
-  {
-    if (type == ResourceType_Instance)
-    {
-      result = id;
-      return true;
-    }
-
-    std::string childId;
-    
-    {
-      std::list<std::string> children;
-      index.GetChildInstances(children, id);
-
-      if (children.empty())
-      {
-        return false;
-      }
-
-      childId = children.front();
-    }
-
-    return LookupOneInstance(result, index, childId, GetChildResourceType(type));
-  }
-
-
-  bool ResourceFinder::Apply(std::list<std::string>& result,
-                             const IQuery& query)
-  {
-    CandidateResources candidates(*this);
-
-    ApplyAtLevel(candidates, query, ResourceType_Patient);
-
-    const ResourceType level = query.GetLevel();
-
-    if (level == ResourceType_Study ||
-        level == ResourceType_Series ||
-        level == ResourceType_Instance)
-    {
-      ApplyAtLevel(candidates, query, ResourceType_Study);
-    }
-        
-    if (level == ResourceType_Series ||
-        level == ResourceType_Instance)
-    {
-      ApplyAtLevel(candidates, query, ResourceType_Series);
-    }
-        
-    if (level == ResourceType_Instance)
-    {
-      ApplyAtLevel(candidates, query, ResourceType_Instance);
-    }
-
-    if (!query.HasInstanceFilter())
-    {
-      candidates.Flatten(result);
-
-      if (maxResults_ != 0 &&
-          result.size() >= maxResults_)
-      {
-        result.resize(maxResults_);
-        return false;
-      }
-      else
-      {
-        return true;
-      }
-    }
-    else
-    {
-      std::list<std::string> tmp;
-      candidates.Flatten(tmp);
-      
-      result.clear();
-      for (std::list<std::string>::const_iterator 
-             resource = tmp.begin(); resource != tmp.end(); ++resource)
-      {
-        try
-        {
-          std::string instance;
-          if (LookupOneInstance(instance, context_.GetIndex(), *resource, level))
-          {
-            Json::Value content;
-            context_.ReadJson(content, instance);
-            if (query.FilterInstance(*resource, content))
-            {
-              result.push_back(*resource);
-
-              if (maxResults_ != 0 &&
-                  result.size() >= maxResults_)
-              {
-                // Too many results, stop before recording this new match
-                return false;
-              }
-            }
-          }
-        }
-        catch (OrthancException&)
-        {
-          // This resource has been deleted since the search was started
-        }
-      }      
-    }
-
-    return true;  // All the matching resources have been returned
-  }
-}
--- a/OrthancServer/ResourceFinder.h	Fri Oct 23 17:04:22 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-/**
- * 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 "ServerIndex.h"
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ResourceFinder : public boost::noncopyable
-  {
-  public:
-    class IQuery : public boost::noncopyable
-    {
-    public:
-      virtual ~IQuery()
-      {
-      }
-
-      virtual ResourceType GetLevel() const = 0;
-
-      virtual bool RestrictIdentifier(std::string& value,
-                                      DicomTag identifier) const = 0;
-
-      virtual bool HasMainDicomTagsFilter(ResourceType level) const = 0;
-
-      virtual bool FilterMainDicomTags(const std::string& resourceId,
-                                       ResourceType level,
-                                       const DicomMap& mainTags) const = 0;
-
-      virtual bool HasInstanceFilter() const = 0;
-
-      virtual bool FilterInstance(const std::string& instanceId,
-                                  const Json::Value& content) const = 0;
-    };
-
-
-  private:
-    typedef std::map<DicomTag, std::string>  Identifiers;
-
-    class CandidateResources;
-
-    ServerContext&    context_;
-    size_t            maxResults_;
-
-    void ApplyAtLevel(CandidateResources& candidates,
-                      const IQuery& query,
-                      ResourceType level);
-
-  public:
-    ResourceFinder(ServerContext& context);
-
-    void SetMaxResults(size_t value)
-    {
-      maxResults_ = value;
-    }
-
-    size_t GetMaxResults() const
-    {
-      return maxResults_;
-    }
-
-    // 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,
-               const IQuery& query);
-  };
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/IFindConstraint.h	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,53 @@
+/**
+ * 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 "LookupIdentifierQuery.h"
+
+namespace Orthanc
+{
+  class IFindConstraint : public boost::noncopyable
+  {
+  public:
+    virtual ~IFindConstraint()
+    {
+    }
+
+    virtual IFindConstraint* Clone() const = 0;
+
+    virtual void Setup(LookupIdentifierQuery& lookup,
+                       const DicomTag& tag) const = 0;
+
+    virtual bool Match(const std::string& value) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/ListConstraint.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,78 @@
+/**
+ * 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 "ListConstraint.h"
+
+
+namespace Orthanc
+{
+  void ListConstraint::AddAllowedValue(const std::string& value)
+  {
+    if (isCaseSensitive_)
+    {
+      allowedValues_.insert(value);
+    }
+    else
+    {
+      std::string s = value;
+      Toolbox::ToUpperCase(s);
+      allowedValues_.insert(s);      
+    }
+  }
+
+
+  void ListConstraint::Setup(LookupIdentifierQuery& lookup, 
+                             const DicomTag& tag) const
+  {
+    LookupIdentifierQuery::Disjunction& target = lookup.AddDisjunction();
+
+    for (std::set<std::string>::const_iterator
+           it = allowedValues_.begin(); it != allowedValues_.end(); ++it)
+    {
+      target.Add(tag, IdentifierConstraintType_Equal, *it);
+    }
+  }
+
+
+  bool ListConstraint::Match(const std::string& value) const
+  {
+    std::string v = value;
+
+    if (!isCaseSensitive_)
+    {
+      Toolbox::ToUpperCase(v);
+    }
+
+    return allowedValues_.find(v) != allowedValues_.end();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/ListConstraint.h	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,71 @@
+/**
+ * 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 "IFindConstraint.h"
+
+#include <set>
+
+namespace Orthanc
+{
+  class ListConstraint : public IFindConstraint
+  {
+  private:
+    std::set<std::string>  allowedValues_;
+    bool                   isCaseSensitive_;
+
+    ListConstraint(const ListConstraint& other) : 
+      allowedValues_(other.allowedValues_),
+      isCaseSensitive_(other.isCaseSensitive_)
+    {
+    }
+
+  public:
+    ListConstraint(bool isCaseSensitive) : 
+      isCaseSensitive_(isCaseSensitive)
+    {
+    }
+
+    void AddAllowedValue(const std::string& value);
+
+    virtual IFindConstraint* Clone() const
+    {
+      return new ListConstraint(*this);
+    }
+
+    virtual void Setup(LookupIdentifierQuery& lookup,
+                       const DicomTag& tag) const;
+
+    virtual bool Match(const std::string& value) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/LookupIdentifierQuery.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,265 @@
+/**
+ * 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 "LookupIdentifierQuery.h"
+
+#include "../../Core/OrthancException.h"
+#include "SetOfResources.h"
+#include "../FromDcmtkBridge.h"
+
+#include <cassert>
+
+
+
+namespace Orthanc
+{
+  static const DicomTag patientIdentifiers[] = 
+  {
+    DICOM_TAG_PATIENT_ID,
+    DICOM_TAG_PATIENT_NAME,
+    DICOM_TAG_PATIENT_BIRTH_DATE
+  };
+
+  static const DicomTag studyIdentifiers[] = 
+  {
+    DICOM_TAG_PATIENT_ID,
+    DICOM_TAG_PATIENT_NAME,
+    DICOM_TAG_PATIENT_BIRTH_DATE,
+    DICOM_TAG_STUDY_INSTANCE_UID,
+    DICOM_TAG_ACCESSION_NUMBER,
+    DICOM_TAG_STUDY_DESCRIPTION,
+    DICOM_TAG_STUDY_DATE
+  };
+
+  static const DicomTag seriesIdentifiers[] = 
+  {
+    DICOM_TAG_SERIES_INSTANCE_UID
+  };
+
+  static const DicomTag instanceIdentifiers[] = 
+  {
+    DICOM_TAG_SOP_INSTANCE_UID
+  };
+
+
+  void LookupIdentifierQuery::LoadIdentifiers(const DicomTag*& tags,
+                                              size_t& size,
+                                              ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        tags = patientIdentifiers;
+        size = sizeof(patientIdentifiers) / sizeof(DicomTag);
+        break;
+
+      case ResourceType_Study:
+        tags = studyIdentifiers;
+        size = sizeof(studyIdentifiers) / sizeof(DicomTag);
+        break;
+
+      case ResourceType_Series:
+        tags = seriesIdentifiers;
+        size = sizeof(seriesIdentifiers) / sizeof(DicomTag);
+        break;
+
+      case ResourceType_Instance:
+        tags = instanceIdentifiers;
+        size = sizeof(instanceIdentifiers) / sizeof(DicomTag);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  LookupIdentifierQuery::Disjunction::~Disjunction()
+  {
+    for (size_t i = 0; i < disjunction_.size(); i++)
+    {
+      delete disjunction_[i];
+    }
+  }
+
+
+  void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag,
+                                               IdentifierConstraintType type,
+                                               const std::string& value)
+  {
+    disjunction_.push_back(new Constraint(tag, type, value));
+  }
+
+
+  LookupIdentifierQuery::~LookupIdentifierQuery()
+  {
+    for (Constraints::iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+
+  bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag,
+                                           ResourceType level)
+  {
+    const DicomTag* tags;
+    size_t size;
+
+    LoadIdentifiers(tags, size, level);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      if (tag == tags[i])
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  void LookupIdentifierQuery::AddConstraint(DicomTag tag,
+                                            IdentifierConstraintType type,
+                                            const std::string& value)
+  {
+    assert(IsIdentifier(tag));
+    constraints_.push_back(new Disjunction);
+    constraints_.back()->Add(tag, type, value);
+  }
+
+
+  LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction()
+  {
+    constraints_.push_back(new Disjunction);
+    return *constraints_.back();
+  }
+
+
+  std::string LookupIdentifierQuery::NormalizeIdentifier(const std::string& value)
+  {
+    std::string s = Toolbox::ConvertToAscii(Toolbox::StripSpaces(value));
+    Toolbox::ToUpperCase(s);
+    return s;
+  }
+
+
+  void LookupIdentifierQuery::StoreIdentifiers(IDatabaseWrapper& database,
+                                               int64_t resource,
+                                               ResourceType level,
+                                               const DicomMap& map)
+  {
+    const DicomTag* tags;
+    size_t size;
+
+    LoadIdentifiers(tags, size, level);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      const DicomValue* value = map.TestAndGetValue(tags[i]);
+      if (value != NULL &&
+          !value->IsNull() &&
+          !value->IsBinary())
+      {
+        std::string s = NormalizeIdentifier(value->GetContent());
+        database.SetIdentifierTag(resource, tags[i], s);
+      }
+    }
+  }
+
+
+  void LookupIdentifierQuery::Apply(std::list<std::string>& result,
+                                    IDatabaseWrapper& database)
+  {
+    SetOfResources resources(database, level_);
+    Apply(resources, database);
+
+    resources.Flatten(result);
+  }
+
+
+  void LookupIdentifierQuery::Apply(SetOfResources& result,
+                                    IDatabaseWrapper& database)
+  {
+    for (size_t i = 0; i < GetSize(); i++)
+    {
+      std::list<int64_t> a;
+
+      for (size_t j = 0; j < constraints_[i]->GetSize(); j++)
+      {
+        const Constraint& constraint = constraints_[i]->GetConstraint(j);
+        std::list<int64_t> b;
+        database.LookupIdentifier(b, level_, constraint.GetTag(), constraint.GetType(), constraint.GetValue());
+
+        a.splice(a.end(), b);
+      }
+
+      result.Intersect(a);
+    }
+  }
+
+
+  void LookupIdentifierQuery::Print(std::ostream& s) const
+  {
+    s << "Constraint: " << std::endl;
+    for (Constraints::const_iterator
+           it = constraints_.begin(); it != constraints_.end(); ++it)
+    {
+      if (it == constraints_.begin())
+        s << "   ";
+      else
+        s << "OR ";
+
+      for (size_t j = 0; j < (*it)->GetSize(); j++)
+      {
+        const Constraint& c = (*it)->GetConstraint(j);
+        s << FromDcmtkBridge::GetName(c.GetTag());
+
+        switch (c.GetType())
+        {
+          case IdentifierConstraintType_Equal: s << " == "; break;
+          case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break;
+          case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break;
+          case IdentifierConstraintType_Wildcard: s << " ~= "; break;
+          default:
+            s << " ? ";
+        }
+
+        s << c.GetValue() << std::endl;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/LookupIdentifierQuery.h	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,183 @@
+/**
+ * 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 "../ServerEnumerations.h"
+#include "../IDatabaseWrapper.h"
+
+#include "SetOfResources.h"
+
+#include <vector>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  /**
+   * Primitive for wildcard matching, as defined in DICOM:
+   * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4
+   * 
+   * "Any occurrence of an "*" or a "?", then "*" shall match any
+   * sequence of characters (including a zero length value) and "?"
+   * shall match any single character. This matching is case
+   * sensitive, except for Attributes with an PN Value
+   * Representation (e.g., Patient Name (0010,0010))."
+   * 
+   * Pay attention to the fact that "*" (resp. "?") generally
+   * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The
+   * values "%", "_", "\" should in the user request should
+   * respectively be escaped as "\%", "\_" and "\\".
+   *
+   * This matching must be case sensitive: The special case of PN VR
+   * is taken into consideration by normalizing the query string in
+   * method "NormalizeIdentifier()".
+   **/
+
+  class LookupIdentifierQuery : public boost::noncopyable
+  {
+  public:
+    class Constraint
+    {
+    private:
+      DicomTag                  tag_;
+      IdentifierConstraintType  type_;
+      std::string               value_;
+
+    public:
+      Constraint(const DicomTag& tag,
+                 IdentifierConstraintType type,
+                 const std::string& value) : 
+        tag_(tag),
+        type_(type),
+        value_(NormalizeIdentifier(value))
+      {
+      }
+
+      const DicomTag& GetTag() const
+      {
+        return tag_;
+      }
+
+      IdentifierConstraintType GetType() const
+      {
+        return type_;
+      }
+      
+      const std::string& GetValue() const
+      {
+        return value_;
+      }
+    };
+
+
+    class Disjunction : public boost::noncopyable
+    {
+    private:
+      std::vector<Constraint*>  disjunction_;
+
+    public:
+      ~Disjunction();
+
+      void Add(const DicomTag& tag,
+               IdentifierConstraintType type,
+               const std::string& value);
+
+      size_t GetSize() const
+      {
+        return disjunction_.size();
+      }
+
+      const Constraint&  GetConstraint(size_t i) const
+      {
+        return *disjunction_[i];
+      }
+    };
+
+
+  private:
+    typedef std::vector<Disjunction*>  Constraints;
+
+    ResourceType  level_;
+    Constraints   constraints_;
+
+    static std::string NormalizeIdentifier(const std::string& value);
+
+  public:
+    LookupIdentifierQuery(ResourceType level) : level_(level)
+    {
+    }
+
+    ~LookupIdentifierQuery();
+
+    bool IsIdentifier(const DicomTag& tag)
+    {
+      return IsIdentifier(tag, level_);
+    }
+
+    void AddConstraint(DicomTag tag,
+                       IdentifierConstraintType type,
+                       const std::string& value);
+
+    Disjunction& AddDisjunction();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    size_t GetSize() const
+    {
+      return constraints_.size();
+    }
+
+    // The database must be locked
+    void Apply(std::list<std::string>& result,
+               IDatabaseWrapper& database);
+
+    void Apply(SetOfResources& result,
+               IDatabaseWrapper& database);
+
+    static void LoadIdentifiers(const DicomTag*& tags,
+                                size_t& size,
+                                ResourceType level);
+
+    static bool IsIdentifier(const DicomTag& tag,
+                             ResourceType level);
+
+    static void StoreIdentifiers(IDatabaseWrapper& database,
+                                 int64_t resource,
+                                 ResourceType level,
+                                 const DicomMap& map);
+
+    void Print(std::ostream& s) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/LookupResource.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,510 @@
+/**
+ * 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 "LookupResource.h"
+
+#include "ListConstraint.h"
+#include "RangeConstraint.h"
+#include "ValueConstraint.h"
+#include "WildcardConstraint.h"
+
+#include "../../Core/OrthancException.h"
+#include "../../Core/FileStorage/StorageAccessor.h"
+#include "../ServerToolbox.h"
+#include "../FromDcmtkBridge.h"
+
+
+namespace Orthanc
+{
+  LookupResource::Level::Level(ResourceType level) : level_(level)
+  {
+    const DicomTag* tags = NULL;
+    size_t size;
+    
+    LookupIdentifierQuery::LoadIdentifiers(tags, size, level);
+    
+    for (size_t i = 0; i < size; i++)
+    {
+      identifiers_.insert(tags[i]);
+    }
+    
+    DicomMap::LoadMainDicomTags(tags, size, level);
+    
+    for (size_t i = 0; i < size; i++)
+    {
+      if (identifiers_.find(tags[i]) == identifiers_.end())
+      {
+        mainTags_.insert(tags[i]);
+      }
+    }    
+  }
+
+  LookupResource::Level::~Level()
+  {
+    for (Constraints::iterator it = mainTagsConstraints_.begin();
+         it != mainTagsConstraints_.end(); ++it)
+    {
+      delete it->second;
+    }
+
+    for (Constraints::iterator it = identifiersConstraints_.begin();
+         it != identifiersConstraints_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+  bool LookupResource::Level::Add(const DicomTag& tag,
+                                  std::auto_ptr<IFindConstraint>& constraint)
+  {
+    if (identifiers_.find(tag) != identifiers_.end())
+    {
+      if (level_ == ResourceType_Patient)
+      {
+        // The filters on the patient level must be cloned to the study level
+        identifiersConstraints_[tag] = constraint->Clone();
+      }
+      else
+      {
+        identifiersConstraints_[tag] = constraint.release();
+      }
+
+      return true;
+    }
+    else if (mainTags_.find(tag) != mainTags_.end())
+    {
+      if (level_ == ResourceType_Patient)
+      {
+        // The filters on the patient level must be cloned to the study level
+        mainTagsConstraints_[tag] = constraint->Clone();
+      }
+      else
+      {
+        mainTagsConstraints_[tag] = constraint.release();
+      }
+
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  LookupResource::LookupResource(ResourceType level) : level_(level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        levels_[ResourceType_Patient] = new Level(ResourceType_Patient);
+        break;
+
+      case ResourceType_Study:
+        levels_[ResourceType_Study] = new Level(ResourceType_Study);
+        // Do not add "break" here
+
+      case ResourceType_Series:
+        levels_[ResourceType_Series] = new Level(ResourceType_Series);
+        // Do not add "break" here
+
+      case ResourceType_Instance:
+        levels_[ResourceType_Instance] = new Level(ResourceType_Instance);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  LookupResource::~LookupResource()
+  {
+    for (Levels::iterator it = levels_.begin();
+         it != levels_.end(); ++it)
+    {
+      delete it->second;
+    }
+
+    for (Constraints::iterator it = unoptimizedConstraints_.begin();
+         it != unoptimizedConstraints_.end(); ++it)
+    {
+      delete it->second;
+    }    
+  }
+
+
+
+  bool LookupResource::AddInternal(ResourceType level,
+                                   const DicomTag& tag,
+                                   std::auto_ptr<IFindConstraint>& constraint)
+  {
+    Levels::iterator it = levels_.find(level);
+    if (it != levels_.end())
+    {
+      if (it->second->Add(tag, constraint))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  void LookupResource::Add(const DicomTag& tag,
+                           IFindConstraint* constraint)
+  {
+    std::auto_ptr<IFindConstraint> c(constraint);
+
+    if (!AddInternal(ResourceType_Patient, tag, c) &&
+        !AddInternal(ResourceType_Study, tag, c) &&
+        !AddInternal(ResourceType_Series, tag, c) &&
+        !AddInternal(ResourceType_Instance, tag, c))
+    {
+      unoptimizedConstraints_[tag] = c.release();
+    }
+  }
+
+
+  static bool Match(const DicomMap& tags,
+                    const DicomTag& tag,
+                    const IFindConstraint& constraint)
+  {
+    const DicomValue* value = tags.TestAndGetValue(tag);
+
+    if (value == NULL ||
+        value->IsNull() ||
+        value->IsBinary())
+    {
+      return false;
+    }
+    else
+    {
+      return constraint.Match(value->GetContent());
+    }
+  }
+
+
+  void LookupResource::Level::Apply(SetOfResources& candidates,
+                                    IDatabaseWrapper& database) const
+  {
+    // First, use the indexed identifiers
+    LookupIdentifierQuery query(level_);
+
+    for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
+         it != identifiersConstraints_.end(); ++it)
+    {
+      it->second->Setup(query, it->first);
+    }
+
+    query.Apply(candidates, database);
+
+    /*{
+      query.Print(std::cout);
+      std::list<int64_t>  source;
+      candidates.Flatten(source);
+      printf("=> %d\n", source.size());
+      }*/
+
+    // Secondly, filter using the main DICOM tags
+    if (!identifiersConstraints_.empty() ||
+        !mainTagsConstraints_.empty())
+    {
+      std::list<int64_t>  source;
+      candidates.Flatten(source);
+      candidates.Clear();
+
+      std::list<int64_t>  filtered;
+      for (std::list<int64_t>::const_iterator candidate = source.begin(); 
+           candidate != source.end(); ++candidate)
+      {
+        DicomMap tags;
+        database.GetMainDicomTags(tags, *candidate);
+
+        bool match = true;
+
+        // Re-apply the identifier constraints, as their "Setup"
+        // method is less restrictive than their "Match" method
+        for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
+             match && it != identifiersConstraints_.end(); ++it)
+        {
+          if (!Match(tags, it->first, *it->second))
+          {
+            match = false;
+          }
+        }
+
+        for (Constraints::const_iterator it = mainTagsConstraints_.begin(); 
+             match && it != mainTagsConstraints_.end(); ++it)
+        {
+          if (!Match(tags, it->first, *it->second))
+          {
+            match = false;
+          }
+        }
+
+        if (match)
+        {
+          filtered.push_back(*candidate);
+        }
+      }
+      
+      candidates.Intersect(filtered);
+    }
+  }
+
+
+
+  bool LookupResource::IsMatch(const Json::Value& dicomAsJson) const
+  {
+    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
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  void LookupResource::ApplyLevel(SetOfResources& candidates,
+                                  ResourceType level,
+                                  IDatabaseWrapper& database) const
+  {
+    Levels::const_iterator it = levels_.find(level);
+    if (it != levels_.end())
+    {
+      it->second->Apply(candidates, database);
+    }
+
+    if (level == ResourceType_Study &&
+        modalitiesInStudy_.get() != NULL)
+    {
+      // There is a constraint on the "ModalitiesInStudy" DICOM
+      // extension. Check out whether one child series has one of the
+      // allowed modalities
+      std::list<int64_t> allStudies, matchingStudies;
+      candidates.Flatten(allStudies);
+ 
+      for (std::list<int64_t>::const_iterator
+             study = allStudies.begin(); study != allStudies.end(); ++study)
+      {
+        std::list<int64_t> childrenSeries;
+        database.GetChildrenInternalId(childrenSeries, *study);
+
+        for (std::list<int64_t>::const_iterator
+               series = childrenSeries.begin(); series != childrenSeries.end(); ++series)
+        {
+          DicomMap tags;
+          database.GetMainDicomTags(tags, *series);
+
+          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
+          if (value != NULL &&
+              !value->IsNull() &&
+              !value->IsBinary())
+          {
+            if (modalitiesInStudy_->Match(value->GetContent()))
+            {
+              matchingStudies.push_back(*study);
+              break;
+            }
+          }
+        }
+      }
+
+      candidates.Intersect(matchingStudies);
+    }
+  }
+
+
+  void LookupResource::FindCandidates(std::list<int64_t>& result,
+                                      IDatabaseWrapper& database) const
+  {
+    ResourceType startingLevel;
+    if (level_ == ResourceType_Patient)
+    {
+      startingLevel = ResourceType_Patient;
+    }
+    else
+    {
+      startingLevel = ResourceType_Study;
+    }
+
+    SetOfResources candidates(database, startingLevel);
+
+    switch (level_)
+    {
+      case ResourceType_Patient:
+        ApplyLevel(candidates, ResourceType_Patient, database);
+        break;
+
+      case ResourceType_Study:
+        ApplyLevel(candidates, ResourceType_Study, database);
+        break;
+
+      case ResourceType_Series:
+        ApplyLevel(candidates, ResourceType_Study, database);
+        candidates.GoDown();
+        ApplyLevel(candidates, ResourceType_Series, database);
+        break;
+
+      case ResourceType_Instance:
+        ApplyLevel(candidates, ResourceType_Study, database);
+        candidates.GoDown();
+        ApplyLevel(candidates, ResourceType_Series, database);
+        candidates.GoDown();
+        ApplyLevel(candidates, ResourceType_Instance, database);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    candidates.Flatten(result);
+  }
+
+
+  void LookupResource::SetModalitiesInStudy(const std::string& modalities)
+  {
+    modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */));
+    
+    std::vector<std::string> items;
+    Toolbox::TokenizeString(items, modalities, '\\');
+    
+    for (size_t i = 0; i < items.size(); i++)
+    {
+      modalitiesInStudy_->AddAllowedValue(items[i]);
+    }
+  }
+
+
+  void LookupResource::AddDicomConstraint(const DicomTag& tag,
+                                          const std::string& dicomQuery,
+                                          bool caseSensitive)
+  {
+    ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag);
+
+    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
+    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
+    if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
+    {
+      SetModalitiesInStudy(dicomQuery);
+    }
+    else if ((vr == ValueRepresentation_Date ||
+              vr == ValueRepresentation_DateTime ||
+              vr == ValueRepresentation_Time) &&
+             dicomQuery.find('-') != std::string::npos)
+    {
+      /**
+       * Range matching is only defined for TM, DA and DT value
+       * representations. This code fixes issues 35 and 37.
+       *
+       * Reference: "Range matching is not defined for types of
+       * Attributes other than dates and times", DICOM PS 3.4,
+       * C.2.2.2.5 ("Range Matching").
+       **/
+      size_t separator = dicomQuery.find('-');
+      std::string lower = dicomQuery.substr(0, separator);
+      std::string upper = dicomQuery.substr(separator + 1);
+      Add(tag, new RangeConstraint(lower, upper, caseSensitive));
+    }
+    else if (dicomQuery.find('\\') != std::string::npos)
+    {
+      std::auto_ptr<ListConstraint> constraint(new ListConstraint(caseSensitive));
+
+      std::vector<std::string> items;
+      Toolbox::TokenizeString(items, dicomQuery, '\\');
+
+      for (size_t i = 0; i < items.size(); i++)
+      {
+        constraint->AddAllowedValue(items[i]);
+      }
+
+      Add(tag, constraint.release());
+    }
+    else if (dicomQuery.find('*') != std::string::npos ||
+             dicomQuery.find('?') != std::string::npos)
+    {
+      Add(tag, new WildcardConstraint(dicomQuery, caseSensitive));
+    }
+    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
+      **/
+
+      Add(tag, new ValueConstraint(dicomQuery, caseSensitive));
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/LookupResource.h	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,107 @@
+/**
+ * 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 "ListConstraint.h"
+#include "SetOfResources.h"
+
+#include <memory>
+
+namespace Orthanc
+{
+  class LookupResource : public boost::noncopyable
+  {
+  private:
+    typedef std::map<DicomTag, IFindConstraint*>  Constraints;
+    
+    class Level
+    {
+    private:
+      ResourceType        level_;
+      std::set<DicomTag>  identifiers_;
+      std::set<DicomTag>  mainTags_;
+      Constraints         identifiersConstraints_;
+      Constraints         mainTagsConstraints_;
+
+    public:
+      Level(ResourceType level);
+
+      ~Level();
+
+      bool Add(const DicomTag& tag,
+               std::auto_ptr<IFindConstraint>& constraint);
+
+      void Apply(SetOfResources& candidates,
+                 IDatabaseWrapper& database) const;
+    };
+
+    typedef std::map<ResourceType, Level*>  Levels;
+
+    ResourceType                    level_;
+    Levels                          levels_;
+    Constraints                     unoptimizedConstraints_; 
+    std::auto_ptr<ListConstraint>   modalitiesInStudy_;
+
+    bool AddInternal(ResourceType level,
+                     const DicomTag& tag,
+                     std::auto_ptr<IFindConstraint>& constraint);
+
+    void ApplyLevel(SetOfResources& candidates,
+                    ResourceType level,
+                    IDatabaseWrapper& database) const;
+
+  public:
+    LookupResource(ResourceType level);
+
+    ~LookupResource();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetModalitiesInStudy(const std::string& modalities); 
+
+    void Add(const DicomTag& tag,
+             IFindConstraint* constraint);   // Takes ownership
+
+    void AddDicomConstraint(const DicomTag& tag,
+                            const std::string& dicomQuery,
+                            bool caseSensitive);
+
+    void FindCandidates(std::list<int64_t>& result,
+                        IDatabaseWrapper& database) const;
+
+    bool IsMatch(const Json::Value& dicomAsJson) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/RangeConstraint.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,90 @@
+/**
+ * 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 "RangeConstraint.h"
+
+#include "../../Core/Toolbox.h"
+
+namespace Orthanc
+{
+  RangeConstraint::RangeConstraint(const std::string& lower,
+                                   const std::string& upper,
+                                   bool isCaseSensitive) : 
+    lower_(lower),
+    upper_(upper),
+    isCaseSensitive_(isCaseSensitive)
+  {
+    if (!isCaseSensitive_)
+    {
+      Toolbox::ToUpperCase(lower_);
+      Toolbox::ToUpperCase(upper_);
+    }
+  }
+
+
+  void RangeConstraint::Setup(LookupIdentifierQuery& lookup,
+                              const DicomTag& tag) const
+  {
+    lookup.AddConstraint(tag, IdentifierConstraintType_GreaterOrEqual, lower_);
+    lookup.AddConstraint(tag, IdentifierConstraintType_SmallerOrEqual, upper_);
+  }
+
+
+  bool RangeConstraint::Match(const std::string& value) const
+  {
+    std::string v = value;
+
+    if (!isCaseSensitive_)
+    {
+      Toolbox::ToUpperCase(v);
+    }
+
+    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_);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/RangeConstraint.h	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,68 @@
+/**
+ * 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 "IFindConstraint.h"
+
+namespace Orthanc
+{
+  class RangeConstraint : public IFindConstraint
+  {
+  private:
+    std::string  lower_;
+    std::string  upper_;
+    bool         isCaseSensitive_;
+
+    RangeConstraint(const RangeConstraint& other) : 
+      lower_(other.lower_),
+      upper_(other.upper_),
+      isCaseSensitive_(other.isCaseSensitive_)
+    {
+    }
+
+  public:
+    RangeConstraint(const std::string& lower,
+                    const std::string& upper,
+                    bool isCaseSensitive);
+
+    virtual IFindConstraint* Clone() const
+    {
+      return new RangeConstraint(*this);
+    }
+
+    virtual void Setup(LookupIdentifierQuery& lookup,
+                       const DicomTag& tag) const;
+
+    virtual bool Match(const std::string& value) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/SetOfResources.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,156 @@
+/**
+ * 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 "SetOfResources.h"
+
+#include "../../Core/OrthancException.h"
+
+
+namespace Orthanc
+{
+  void SetOfResources::Intersect(const std::list<int64_t>& resources)
+  {
+    if (resources_.get() == NULL)
+    {
+      resources_.reset(new Resources);
+
+      for (std::list<int64_t>::const_iterator
+             it = resources.begin(); it != resources.end(); ++it)
+      {
+        resources_->insert(*it);
+      }
+    }
+    else
+    {
+      std::auto_ptr<Resources> filtered(new Resources);
+
+      for (std::list<int64_t>::const_iterator
+             it = resources.begin(); it != resources.end(); ++it)
+      {
+        if (resources_->find(*it) != resources_->end())
+        {
+          filtered->insert(*it);
+        }
+      }
+
+      resources_ = filtered;
+    }
+  }
+
+
+  void SetOfResources::GoDown()
+  {
+    if (level_ == ResourceType_Instance)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (resources_.get() != NULL)
+    {
+      std::auto_ptr<Resources> children(new Resources);
+
+      for (Resources::const_iterator it = resources_->begin(); 
+           it != resources_->end(); ++it)
+      {
+        std::list<int64_t> tmp;
+        database_.GetChildrenInternalId(tmp, *it);
+
+        for (std::list<int64_t>::const_iterator
+               child = tmp.begin(); child != tmp.end(); ++child)
+        {
+          children->insert(*child);
+        }
+      }
+
+      resources_ = children;
+    }
+
+    switch (level_)
+    {
+      case ResourceType_Patient:
+        level_ = ResourceType_Study;
+        break;
+
+      case ResourceType_Study:
+        level_ = ResourceType_Series;
+        break;
+
+      case ResourceType_Series:
+        level_ = ResourceType_Instance;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  void SetOfResources::Flatten(std::list<std::string>& result)
+  {
+    result.clear();
+      
+    if (resources_.get() == NULL)
+    {
+      // All the resources of this level are part of the filter
+      database_.GetAllPublicIds(result, level_);
+    }
+    else
+    {
+      for (Resources::const_iterator it = resources_->begin(); 
+           it != resources_->end(); ++it)
+      {
+        result.push_back(database_.GetPublicId(*it));
+      }
+    }
+  }
+
+
+  void SetOfResources::Flatten(std::list<int64_t>& result)
+  {
+    result.clear();
+      
+    if (resources_.get() == NULL)
+    {
+      // All the resources of this level are part of the filter
+      database_.GetAllInternalIds(result, level_);
+    }
+    else
+    {
+      for (Resources::const_iterator it = resources_->begin(); 
+           it != resources_->end(); ++it)
+      {
+        result.push_back(*it);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/SetOfResources.h	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,78 @@
+/**
+ * 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 "../IDatabaseWrapper.h"
+
+#include <set>
+#include <boost/noncopyable.hpp>
+#include <memory>
+
+namespace Orthanc
+{
+  class SetOfResources : public boost::noncopyable
+  {
+  private:
+    typedef std::set<int64_t>  Resources;
+
+    IDatabaseWrapper&         database_;
+    ResourceType              level_;
+    std::auto_ptr<Resources>  resources_;
+    
+  public:
+    SetOfResources(IDatabaseWrapper& database,
+                   ResourceType level) : 
+      database_(database),
+      level_(level)
+    {
+    }
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void Intersect(const std::list<int64_t>& resources);
+
+    void GoDown();
+
+    void Flatten(std::list<int64_t>& result);
+
+    void Flatten(std::list<std::string>& result);
+
+    void Clear()
+    {
+      resources_.reset(NULL);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/ValueConstraint.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,73 @@
+/**
+ * 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 "ValueConstraint.h"
+
+#include "../../Core/Toolbox.h"
+
+#include <stdio.h>
+
+namespace Orthanc
+{
+  ValueConstraint::ValueConstraint(const std::string& value,
+                                   bool isCaseSensitive) : 
+    value_(value),
+    isCaseSensitive_(isCaseSensitive)
+  {
+    if (!isCaseSensitive)
+    {
+      Toolbox::ToUpperCase(value_);
+    }
+  }
+
+
+  void ValueConstraint::Setup(LookupIdentifierQuery& lookup,
+                              const DicomTag& tag) const
+  {
+    lookup.AddConstraint(tag, IdentifierConstraintType_Equal, value_);
+  }
+
+  bool ValueConstraint::Match(const std::string& value) const
+  {
+    if (isCaseSensitive_)
+    {
+      return value_ == value;
+    }
+    else
+    {
+      std::string v;
+      Toolbox::ToUpperCase(v, value);
+      return value_ == v;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/ValueConstraint.h	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,65 @@
+/**
+ * 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 "IFindConstraint.h"
+
+namespace Orthanc
+{
+  class ValueConstraint : public IFindConstraint
+  {
+  private:
+    std::string  value_;
+    bool         isCaseSensitive_;
+
+    ValueConstraint(const ValueConstraint& other) : 
+      value_(other.value_),
+      isCaseSensitive_(other.isCaseSensitive_)
+    {
+    }
+
+  public:
+    ValueConstraint(const std::string& value,
+                    bool isCaseSensitive);
+
+    virtual IFindConstraint* Clone() const
+    {
+      return new ValueConstraint(*this);
+    }
+
+    virtual void Setup(LookupIdentifierQuery& lookup,
+                       const DicomTag& tag) const;
+
+    virtual bool Match(const std::string& value) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/WildcardConstraint.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,81 @@
+/**
+ * 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 "WildcardConstraint.h"
+
+#include <boost/regex.hpp>
+
+namespace Orthanc
+{
+  struct WildcardConstraint::PImpl
+  {
+    boost::regex  pattern_;
+    std::string   wildcard_;
+  };
+
+
+  WildcardConstraint::WildcardConstraint(const WildcardConstraint& other) :
+    pimpl_(new PImpl(*other.pimpl_))
+  {
+  }
+
+
+  WildcardConstraint::WildcardConstraint(const std::string& wildcard,
+                                         bool isCaseSensitive) :
+    pimpl_(new PImpl)
+  {
+    pimpl_->wildcard_ = wildcard;
+
+    std::string re = Toolbox::WildcardToRegularExpression(wildcard);
+
+    if (isCaseSensitive)
+    {
+      pimpl_->pattern_ = boost::regex(re);
+    }
+    else
+    {
+      pimpl_->pattern_ = boost::regex(re, boost::regex::icase /* case insensitive search */);
+    }
+  }
+
+  bool WildcardConstraint::Match(const std::string& value) const
+  {
+    return boost::regex_match(value, pimpl_->pattern_);
+  }
+
+  void WildcardConstraint::Setup(LookupIdentifierQuery& lookup,
+                                 const DicomTag& tag) const
+  {
+    lookup.AddConstraint(tag, IdentifierConstraintType_Wildcard, pimpl_->wildcard_);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/WildcardConstraint.h	Thu Oct 29 12:45:20 2015 +0100
@@ -0,0 +1,63 @@
+/**
+ * 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 "IFindConstraint.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class WildcardConstraint : public IFindConstraint
+  {
+  private:
+    struct PImpl;
+    boost::shared_ptr<PImpl>  pimpl_;
+
+    WildcardConstraint(const WildcardConstraint& other);
+
+  public:
+    WildcardConstraint(const std::string& wildcard,
+                       bool isCaseSensitive);
+
+    virtual IFindConstraint* Clone() const
+    {
+      return new WildcardConstraint(*this);
+    }
+
+    virtual void Setup(LookupIdentifierQuery& lookup,
+                       const DicomTag& tag) const;
+
+    virtual bool Match(const std::string& value) const;
+  };
+}
--- a/OrthancServer/ServerContext.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/ServerContext.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -543,4 +543,39 @@
     return false;
 #endif
   }
+
+
+  bool ServerContext::Apply(std::list<std::string>& result,
+                            const ::Orthanc::LookupResource& lookup,
+                            size_t maxResults)
+  {
+    result.clear();
+
+    std::vector<std::string> resources, instances;
+    GetIndex().FindCandidates(resources, instances, lookup);
+
+    assert(resources.size() == instances.size());
+
+    for (size_t i = 0; i < instances.size(); i++)
+    {
+      Json::Value dicom;
+      ReadJson(dicom, instances[i]);
+      
+      if (lookup.IsMatch(dicom))
+      {
+        if (maxResults != 0 &&
+            result.size() >= maxResults)
+        {
+          return false;  // too many results
+        }
+        else
+        {
+          result.push_back(resources[i]);
+        }
+      }
+    }
+
+    return true;  // finished
+  }
+
 }
--- a/OrthancServer/ServerContext.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/ServerContext.h	Thu Oct 29 12:45:20 2015 +0100
@@ -47,6 +47,7 @@
 #include "Scheduler/ServerScheduler.h"
 #include "ServerIndex.h"
 #include "OrthancHttpHandler.h"
+#include "Search/LookupResource.h"
 
 #include <boost/filesystem.hpp>
 #include <boost/thread.hpp>
@@ -245,6 +246,10 @@
 
     void Stop();
 
+    bool Apply(std::list<std::string>& result,
+               const ::Orthanc::LookupResource& lookup,
+               size_t maxResults);
+
 
     /**
      * Management of the plugins
@@ -261,6 +266,5 @@
 #endif
 
     bool HasPlugins() const;
-
   };
 }
--- a/OrthancServer/ServerEnumerations.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/ServerEnumerations.h	Thu Oct 29 12:45:20 2015 +0100
@@ -125,6 +125,14 @@
                                  DicomToJsonFlags_ConvertBinaryToNull)
   };
 
+  enum IdentifierConstraintType
+  {
+    IdentifierConstraintType_Equal,
+    IdentifierConstraintType_SmallerOrEqual,
+    IdentifierConstraintType_GreaterOrEqual,
+    IdentifierConstraintType_Wildcard        /* Case sensitive, "*" or "?" are the only allowed wildcards */
+  };
+
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
--- a/OrthancServer/ServerIndex.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/ServerIndex.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -45,6 +45,8 @@
 #include "../Core/Logging.h"
 #include "../Core/Uuid.h"
 #include "../Core/DicomFormat/DicomArray.h"
+#include "Search/LookupIdentifierQuery.h"
+#include "Search/LookupResource.h"
 
 #include "FromDcmtkBridge.h"
 #include "ServerContext.h"
@@ -1895,32 +1897,24 @@
 
 
 
-  void ServerIndex::LookupIdentifier(std::list<std::string>& result,
-                                     const DicomTag& tag,
-                                     const std::string& value,
-                                     ResourceType type)
+  void ServerIndex::LookupIdentifierExact(std::list<std::string>& result,
+                                          ResourceType level,
+                                          const DicomTag& tag,
+                                          const std::string& value)
   {
-    assert(tag == DICOM_TAG_PATIENT_ID ||
-           tag == DICOM_TAG_STUDY_INSTANCE_UID ||
-           tag == DICOM_TAG_SERIES_INSTANCE_UID ||
-           tag == DICOM_TAG_SOP_INSTANCE_UID ||
-           tag == DICOM_TAG_ACCESSION_NUMBER);
+    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));
     
     result.clear();
 
     boost::mutex::scoped_lock lock(mutex_);
 
-    std::list<int64_t> id;
-    db_.LookupIdentifier(id, tag, value);
-
-    for (std::list<int64_t>::const_iterator 
-           it = id.begin(); it != id.end(); ++it)
-    {
-      if (db_.GetResourceType(*it) == type)
-      {
-        result.push_back(db_.GetPublicId(*it));
-      }
-    }
+    LookupIdentifierQuery query(level);
+    query.AddConstraint(tag, IdentifierConstraintType_Equal, value);
+    query.Apply(result, db_);
   }
 
 
@@ -2119,4 +2113,34 @@
     boost::mutex::scoped_lock lock(mutex_);
     return db_.GetDatabaseVersion();
   }
+
+
+  void ServerIndex::FindCandidates(std::vector<std::string>& resources,
+                                   std::vector<std::string>& instances,
+                                   const ::Orthanc::LookupResource& lookup)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+   
+    std::list<int64_t> tmp;
+    lookup.FindCandidates(tmp, db_);
+
+    resources.resize(tmp.size());
+    instances.resize(tmp.size());
+
+    size_t pos = 0;
+    for (std::list<int64_t>::const_iterator
+           it = tmp.begin(); it != tmp.end(); ++it, pos++)
+    {
+      assert(db_.GetResourceType(*it) == lookup.GetLevel());
+      
+      int64_t instance;
+      if (!Toolbox::FindOneChildInstance(instance, db_, *it, lookup.GetLevel()))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      resources[pos] = db_.GetPublicId(*it);
+      instances[pos] = db_.GetPublicId(instance);
+    }
+  }
 }
--- a/OrthancServer/ServerIndex.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/ServerIndex.h	Thu Oct 29 12:45:20 2015 +0100
@@ -45,6 +45,7 @@
 
 namespace Orthanc
 {
+  class LookupResource;
   class ServerContext;
 
   class ServerIndex : public boost::noncopyable
@@ -235,10 +236,10 @@
                        /* out */ unsigned int& countInstances, 
                        const std::string& publicId);
 
-    void LookupIdentifier(std::list<std::string>& result,
-                          const DicomTag& tag,
-                          const std::string& value,
-                          ResourceType type);
+    void LookupIdentifierExact(std::list<std::string>& result,
+                               ResourceType level,
+                               const DicomTag& tag,
+                               const std::string& value);
 
     StoreStatus AddAttachment(const FileInfo& attachment,
                               const std::string& publicId);
@@ -261,5 +262,9 @@
                             const std::string& publicId);
 
     unsigned int GetDatabaseVersion();
+
+    void FindCandidates(std::vector<std::string>& resources,
+                        std::vector<std::string>& instances,
+                        const ::Orthanc::LookupResource& lookup);
   };
 }
--- a/OrthancServer/ServerToolbox.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/ServerToolbox.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -38,6 +38,7 @@
 #include "../Core/Logging.h"
 #include "../Core/OrthancException.h"
 #include "ParsedDicomFile.h"
+#include "Search/LookupIdentifierQuery.h"
 
 #include <cassert>
 
@@ -193,45 +194,6 @@
     }
 
 
-    static void SetIdentifierTagInternal(IDatabaseWrapper& database,
-                                         int64_t resource,
-                                         const DicomMap& tags,
-                                         const DicomTag& tag)
-    {
-      const DicomValue* value = tags.TestAndGetValue(tag);
-      if (value != NULL &&
-          !value->IsNull() &&
-          !value->IsBinary())
-      {
-        std::string s = value->GetContent();
-
-        if (tag != DICOM_TAG_PATIENT_ID &&
-            tag != DICOM_TAG_STUDY_INSTANCE_UID &&
-            tag != DICOM_TAG_SERIES_INSTANCE_UID &&
-            tag != DICOM_TAG_SOP_INSTANCE_UID &&
-            tag != DICOM_TAG_ACCESSION_NUMBER)
-        {
-          s = NormalizeIdentifierTag(s);
-        }
-
-        database.SetIdentifierTag(resource, tag, s);
-      }
-    }
-
-
-    static void AttachPatientInformation(IDatabaseWrapper& database,
-                                         int64_t resource,
-                                         const DicomMap& dicomSummary)
-    {
-      DicomMap tags;
-      dicomSummary.ExtractPatientInformation(tags);
-      SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_PATIENT_ID);
-      SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_PATIENT_NAME);
-      SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_PATIENT_BIRTH_DATE);
-      SetMainDicomTagsInternal(database, resource, tags);
-    }
-
-
     void SetMainDicomTags(IDatabaseWrapper& database,
                           int64_t resource,
                           ResourceType level,
@@ -239,33 +201,30 @@
     {
       // WARNING: The database should be locked with a transaction!
 
+      LookupIdentifierQuery::StoreIdentifiers(database, resource, level, dicomSummary);
+
       DicomMap tags;
 
       switch (level)
       {
         case ResourceType_Patient:
-          AttachPatientInformation(database, resource, dicomSummary);
+          dicomSummary.ExtractPatientInformation(tags);
           break;
 
         case ResourceType_Study:
           // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6)
-          AttachPatientInformation(database, resource, dicomSummary);
+          dicomSummary.ExtractPatientInformation(tags);
+          SetMainDicomTagsInternal(database, resource, tags);
 
           dicomSummary.ExtractStudyInformation(tags);
-          SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_STUDY_INSTANCE_UID);
-          SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_ACCESSION_NUMBER);
-          SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_STUDY_DESCRIPTION);
-          SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_STUDY_DATE);
           break;
 
         case ResourceType_Series:
           dicomSummary.ExtractSeriesInformation(tags);
-          SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_SERIES_INSTANCE_UID);
           break;
 
         case ResourceType_Instance:
           dicomSummary.ExtractInstanceInformation(tags);
-          SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_SOP_INSTANCE_UID);
           break;
 
         default:
@@ -374,13 +333,5 @@
         Toolbox::SetMainDicomTags(database, resource, level, dicomSummary);
       }
     }
-
-
-    std::string NormalizeIdentifierTag(const std::string& value)
-    {
-      std::string s = Toolbox::ConvertToAscii(Toolbox::StripSpaces(value));
-      Toolbox::ToUpperCase(s);
-      return s;
-    }
   }
 }
--- a/OrthancServer/ServerToolbox.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/ServerToolbox.h	Thu Oct 29 12:45:20 2015 +0100
@@ -59,7 +59,5 @@
     void ReconstructMainDicomTags(IDatabaseWrapper& database,
                                   IStorageArea& storageArea,
                                   ResourceType level);
-
-    std::string NormalizeIdentifierTag(const std::string& value);
   }
 }
--- a/OrthancServer/main.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/OrthancServer/main.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -491,6 +491,7 @@
     PrintErrorCode(ErrorCode_BadFont, "Badly formatted font file");
     PrintErrorCode(ErrorCode_DatabasePlugin, "The plugin implementing a custom database back-end does not fulfill the proper interface");
     PrintErrorCode(ErrorCode_StorageAreaPlugin, "Error in the plugin implementing a custom storage area");
+    PrintErrorCode(ErrorCode_EmptyRequest, "The request is empty");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
--- a/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Engine/OrthancPluginDatabase.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -274,6 +274,21 @@
   }
 
 
+  void OrthancPluginDatabase::GetAllInternalIds(std::list<int64_t>& target,
+                                                ResourceType resourceType)
+  {
+    if (extensions_.getAllInternalIds == NULL)
+    {
+      LOG(ERROR) << "The database plugin does not implement the GetAllInternalIds primitive";
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+
+    ResetAnswers();
+    CheckSuccess(extensions_.getAllInternalIds(GetContext(), payload_, Plugins::Convert(resourceType)));
+    ForwardAnswers(target);
+  }
+
+
   void OrthancPluginDatabase::GetAllPublicIds(std::list<std::string>& target,
                                               ResourceType resourceType)
   {
@@ -594,20 +609,27 @@
   }
 
 
-  void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& target,
+  void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& result,
+                                               ResourceType level,
                                                const DicomTag& tag,
+                                               IdentifierConstraintType type,
                                                const std::string& value)
   {
-    ResetAnswers();
+    if (extensions_.lookupIdentifier3 == NULL)
+    {
+      LOG(ERROR) << "The database plugin does not implement the LookupIdentifier3 primitive";
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
 
     OrthancPluginDicomTag tmp;
     tmp.group = tag.GetGroup();
     tmp.element = tag.GetElement();
     tmp.value = value.c_str();
 
-    CheckSuccess(backend_.lookupIdentifier(GetContext(), payload_, &tmp));
-
-    ForwardAnswers(target);
+    ResetAnswers();
+    CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level),
+                                               &tmp, Plugins::Convert(type)));
+    ForwardAnswers(result);
   }
 
 
--- a/Plugins/Engine/OrthancPluginDatabase.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Engine/OrthancPluginDatabase.h	Thu Oct 29 12:45:20 2015 +0100
@@ -140,6 +140,9 @@
     virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
                                 int64_t id);
 
+    virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                   ResourceType resourceType);
+
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType);
 
@@ -203,8 +206,10 @@
     virtual bool LookupGlobalProperty(std::string& target,
                                       GlobalProperty property);
 
-    virtual void LookupIdentifier(std::list<int64_t>& target,
+    virtual void LookupIdentifier(std::list<int64_t>& result,
+                                  ResourceType level,
                                   const DicomTag& tag,
+                                  IdentifierConstraintType type,
                                   const std::string& value);
 
     virtual bool LookupMetadata(std::string& target,
--- a/Plugins/Engine/OrthancPlugins.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -302,6 +302,7 @@
         sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) ||
         sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) ||
@@ -911,7 +912,7 @@
     CheckContextAvailable();
 
     std::list<std::string> result;
-    pimpl_->context_->GetIndex().LookupIdentifier(result, tag, p.argument, level);
+    pimpl_->context_->GetIndex().LookupIdentifierExact(result, level, tag, p.argument);
 
     if (result.size() == 1)
     {
--- a/Plugins/Engine/PluginsEnumerations.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Engine/PluginsEnumerations.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -229,6 +229,50 @@
     }
 
 
+    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint)
+    {
+      switch (constraint)
+      {
+        case IdentifierConstraintType_Equal:
+          return OrthancPluginIdentifierConstraint_Equal;
+
+        case IdentifierConstraintType_GreaterOrEqual:
+          return OrthancPluginIdentifierConstraint_GreaterOrEqual;
+
+        case IdentifierConstraintType_SmallerOrEqual:
+          return OrthancPluginIdentifierConstraint_SmallerOrEqual;
+
+        case IdentifierConstraintType_Wildcard:
+          return OrthancPluginIdentifierConstraint_Wildcard;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint)
+    {
+      switch (constraint)
+      {
+        case OrthancPluginIdentifierConstraint_Equal:
+          return IdentifierConstraintType_Equal;
+
+        case OrthancPluginIdentifierConstraint_GreaterOrEqual:
+          return IdentifierConstraintType_GreaterOrEqual;
+
+        case OrthancPluginIdentifierConstraint_SmallerOrEqual:
+          return IdentifierConstraintType_SmallerOrEqual;
+
+        case OrthancPluginIdentifierConstraint_Wildcard:
+          return IdentifierConstraintType_Wildcard;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
 #if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0
     DcmEVR Convert(OrthancPluginValueRepresentation vr)
     {
--- a/Plugins/Engine/PluginsEnumerations.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Engine/PluginsEnumerations.h	Thu Oct 29 12:45:20 2015 +0100
@@ -61,6 +61,10 @@
 
     DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format);
 
+    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint);
+
+    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint);
+
 #if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0
     DcmEVR Convert(OrthancPluginValueRepresentation vr);
 #endif
--- a/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Thu Oct 29 12:45:20 2015 +0100
@@ -522,7 +522,9 @@
       void* payload,
       int32_t property);
 
-    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    /* Use "OrthancPluginDatabaseExtensions::lookupIdentifier3" 
+       instead of this function as of Orthanc 0.9.5 (db v6), can be set to NULL.
+       Output: Use OrthancPluginDatabaseAnswerInt64() */
     OrthancPluginErrorCode  (*lookupIdentifier) (
       /* outputs */
       OrthancPluginDatabaseContext* context,
@@ -661,6 +663,24 @@
       /* inputs */
       void* payload,
       int64_t id);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*getAllInternalIds) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType resourceType);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*lookupIdentifier3) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType resourceType,
+      const OrthancPluginDicomTag* tag,
+      OrthancPluginIdentifierConstraint constraint);
    } OrthancPluginDatabaseExtensions;
 
 /*<! @endcond */
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Oct 29 12:45:20 2015 +0100
@@ -212,6 +212,7 @@
     OrthancPluginErrorCode_BadFont = 30    /*!< Badly formatted font file */,
     OrthancPluginErrorCode_DatabasePlugin = 31    /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */,
     OrthancPluginErrorCode_StorageAreaPlugin = 32    /*!< Error in the plugin implementing a custom storage area */,
+    OrthancPluginErrorCode_EmptyRequest = 33    /*!< The request is empty */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -679,6 +680,22 @@
 
 
   /**
+   * The constraints on the DICOM identifiers that must be supported
+   * by the database plugins.
+   **/
+  typedef enum
+  {
+    OrthancPluginIdentifierConstraint_Equal,           /*!< Equal */
+    OrthancPluginIdentifierConstraint_SmallerOrEqual,  /*!< Less or equal */
+    OrthancPluginIdentifierConstraint_GreaterOrEqual,  /*!< More or equal */
+    OrthancPluginIdentifierConstraint_Wildcard,        /*!< Case-sensitive wildcard matching (with * and ?) */
+
+    _OrthancPluginIdentifierConstraint_INTERNAL = 0x7fffffff
+  } OrthancPluginIdentifierConstraint;
+
+
+
+  /**
    * @brief A memory buffer allocated by the core system of Orthanc.
    *
    * A memory buffer allocated by the core system of Orthanc. When the
@@ -891,7 +908,8 @@
         sizeof(int32_t) != sizeof(OrthancPluginImageFormat) ||
         sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) ||
-        sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags))
+        sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) ||
+        sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint))
     {
       /* Mismatch in the size of the enumerations */
       return 0;
--- a/Plugins/Include/orthanc/OrthancCppDatabasePlugin.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Include/orthanc/OrthancCppDatabasePlugin.h	Thu Oct 29 12:45:20 2015 +0100
@@ -339,6 +339,9 @@
 
     virtual void DeleteResource(int64_t id) = 0;
 
+    virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                   OrthancPluginResourceType resourceType) = 0;
+
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  OrthancPluginResourceType resourceType) = 0;
 
@@ -403,15 +406,11 @@
     virtual bool LookupGlobalProperty(std::string& target /*out*/,
                                       int32_t property) = 0;
 
-    /**
-     * "Identifiers" are necessarily one of the following tags:
-     * PatientID (0x0010, 0x0020), StudyInstanceUID (0x0020, 0x000d),
-     * SeriesInstanceUID (0x0020, 0x000e), SOPInstanceUID (0x0008,
-     * 0x0018) or AccessionNumber (0x0008, 0x0050).
-     **/
     virtual void LookupIdentifier(std::list<int64_t>& target /*out*/,
+                                  OrthancPluginResourceType resourceType,
                                   uint16_t group,
                                   uint16_t element,
+                                  OrthancPluginIdentifierConstraint constraint,
                                   const char* value) = 0;
 
     virtual bool LookupMetadata(std::string& target /*out*/,
@@ -683,6 +682,39 @@
     }
 
 
+    static OrthancPluginErrorCode  GetAllInternalIds(OrthancPluginDatabaseContext* context,
+                                                     void* payload,
+                                                     OrthancPluginResourceType resourceType)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::list<int64_t> target;
+        backend->GetAllInternalIds(target, resourceType);
+
+        for (std::list<int64_t>::const_iterator
+               it = target.begin(); it != target.end(); ++it)
+        {
+          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
+                                           backend->GetOutput().database_, *it);
+        }
+
+        return OrthancPluginErrorCode_Success;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return OrthancPluginErrorCode_DatabasePlugin;
+      }
+      catch (DatabaseException& e)
+      {
+        return e.GetErrorCode();
+      }
+    }
+
+
     static OrthancPluginErrorCode  GetAllPublicIds(OrthancPluginDatabaseContext* context,
                                                    void* payload,
                                                    OrthancPluginResourceType resourceType)
@@ -1295,9 +1327,11 @@
     }
 
 
-    static OrthancPluginErrorCode  LookupIdentifier(OrthancPluginDatabaseContext* context,
-                                                    void* payload,
-                                                    const OrthancPluginDicomTag* tag)
+    static OrthancPluginErrorCode  LookupIdentifier3(OrthancPluginDatabaseContext* context,
+                                                     void* payload,
+                                                     OrthancPluginResourceType resourceType,
+                                                     const OrthancPluginDicomTag* tag,
+                                                     OrthancPluginIdentifierConstraint constraint)
     {
       IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
       backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
@@ -1305,7 +1339,7 @@
       try
       {
         std::list<int64_t> target;
-        backend->LookupIdentifier(target, tag->group, tag->element, tag->value);
+        backend->LookupIdentifier(target, resourceType, tag->group, tag->element, constraint, tag->value);
 
         for (std::list<int64_t>::const_iterator
                it = target.begin(); it != target.end(); ++it)
@@ -1824,7 +1858,7 @@
       params.logExportedResource = LogExportedResource;
       params.lookupAttachment = LookupAttachment;
       params.lookupGlobalProperty = LookupGlobalProperty;
-      params.lookupIdentifier = LookupIdentifier;
+      params.lookupIdentifier = NULL;   // Unused starting with Orthanc 0.9.5 (db v6)
       params.lookupIdentifier2 = NULL;   // Unused starting with Orthanc 0.9.5 (db v6)
       params.lookupMetadata = LookupMetadata;
       params.lookupParent = LookupParent;
@@ -1846,6 +1880,8 @@
       extensions.getDatabaseVersion = GetDatabaseVersion;
       extensions.upgradeDatabase = UpgradeDatabase;
       extensions.clearMainDicomTags = ClearMainDicomTags;
+      extensions.getAllInternalIds = GetAllInternalIds;   // New in Orthanc 0.9.5 (db v6)
+      extensions.lookupIdentifier3 = LookupIdentifier3;   // New in Orthanc 0.9.5 (db v6)
 
       OrthancPluginDatabaseContext* database = OrthancPluginRegisterDatabaseBackendV2(context, &params, &extensions, &backend);
       if (!context)
--- a/Plugins/Samples/DatabasePlugin/CMakeLists.txt	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Samples/DatabasePlugin/CMakeLists.txt	Thu Oct 29 12:45:20 2015 +0100
@@ -46,6 +46,7 @@
   ${ORTHANC_ROOT}/Core/DicomFormat/DicomArray.cpp
   ${ORTHANC_ROOT}/Core/DicomFormat/DicomMap.cpp
   ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp
+  ${ORTHANC_ROOT}/Core/DicomFormat/DicomValue.cpp
   ${ORTHANC_ROOT}/Core/Enumerations.cpp
   ${ORTHANC_ROOT}/Core/SQLite/Connection.cpp
   ${ORTHANC_ROOT}/Core/SQLite/FunctionContext.cpp
--- a/Plugins/Samples/DatabasePlugin/Database.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Samples/DatabasePlugin/Database.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -340,7 +340,7 @@
   {
     GetOutput().AnswerDicomTag(arr.GetElement(i).GetTag().GetGroup(),
                                arr.GetElement(i).GetTag().GetElement(),
-                               arr.GetElement(i).GetValue().AsString());
+                               arr.GetElement(i).GetValue().GetContent());
   }
 }
 
--- a/Plugins/Samples/DatabasePlugin/Database.h	Fri Oct 23 17:04:22 2015 +0200
+++ b/Plugins/Samples/DatabasePlugin/Database.h	Thu Oct 29 12:45:20 2015 +0100
@@ -99,6 +99,12 @@
 
   virtual void DeleteResource(int64_t id);
 
+  virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                 OrthancPluginResourceType resourceType)
+  {
+    base_.GetAllInternalIds(target, Orthanc::Plugins::Convert(resourceType));
+  }
+
   virtual void GetAllPublicIds(std::list<std::string>& target,
                                OrthancPluginResourceType resourceType)
   {
@@ -188,11 +194,15 @@
   }
 
   virtual void LookupIdentifier(std::list<int64_t>& target /*out*/,
+                                OrthancPluginResourceType level,
                                 uint16_t group,
                                 uint16_t element,
+                                OrthancPluginIdentifierConstraint constraint,
                                 const char* value)
   {
-    base_.LookupIdentifier(target, Orthanc::DicomTag(group, element), value);
+    base_.LookupIdentifier(target, Orthanc::Plugins::Convert(level),
+                           Orthanc::DicomTag(group, element), 
+                           Orthanc::Plugins::Convert(constraint), value);
   }
 
   virtual bool LookupMetadata(std::string& target /*out*/,
--- a/Resources/Configuration.json	Fri Oct 23 17:04:22 2015 +0200
+++ b/Resources/Configuration.json	Thu Oct 29 12:45:20 2015 +0100
@@ -258,8 +258,9 @@
   // deleted as new requests are issued.
   "QueryRetrieveSize" : 10,
 
-  // When handling a C-Find SCP request, setting this flag to "false"
-  // will enable case-insensitive match for PN value representation
-  // (such as PatientName). By default, the search is case-insensitive.
+  // When handling a C-Find SCP request, setting this flag to "true"
+  // will enable case-sensitive match for PN value representation
+  // (such as PatientName). By default, the search is
+  // case-insensitive, which does not follow the DICOM standard.
   "CaseSensitivePN" : false
 }
--- a/Resources/ErrorCodes.json	Fri Oct 23 17:04:22 2015 +0200
+++ b/Resources/ErrorCodes.json	Thu Oct 29 12:45:20 2015 +0100
@@ -185,6 +185,11 @@
     "Name": "StorageAreaPlugin", 
     "Description": "Error in the plugin implementing a custom storage area"
   },
+  {
+    "Code": 33,
+    "Name": "EmptyRequest",
+    "Description": "The request is empty"
+  },
 
 
 
--- a/UnitTestsSources/ServerIndexTests.cpp	Fri Oct 23 17:04:22 2015 +0200
+++ b/UnitTestsSources/ServerIndexTests.cpp	Thu Oct 29 12:45:20 2015 +0100
@@ -39,6 +39,7 @@
 #include "../OrthancServer/DatabaseWrapper.h"
 #include "../OrthancServer/ServerContext.h"
 #include "../OrthancServer/ServerIndex.h"
+#include "../OrthancServer/Search/LookupIdentifierQuery.h"
 
 #include <ctype.h>
 #include <algorithm>
@@ -245,6 +246,18 @@
           throw OrthancException(ErrorCode_InternalError);
       }
     }
+
+
+    void DoLookup(std::list<std::string>& result,
+                  ResourceType level,
+                  const DicomTag& tag,
+                  const std::string& value)
+    {
+      LookupIdentifierQuery query(level);
+      query.AddConstraint(tag, IdentifierConstraintType_Equal, value);
+      query.Apply(result, *index_);
+    }
+
   };
 }
 
@@ -690,36 +703,64 @@
   index_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0");
   index_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0");
 
-  std::list<int64_t> s;
+  std::list<std::string> s;
 
-  index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "0");
+  DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "0");
   ASSERT_EQ(2u, s.size());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "a") != s.end());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "c") != s.end());
 
-  index_->LookupIdentifier(s, DICOM_TAG_SERIES_INSTANCE_UID, "0");
+  DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "0");
   ASSERT_EQ(1u, s.size());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), a[3]) != s.end());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "d") != s.end());
 
-  index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1");
+  DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1");
   ASSERT_EQ(1u, s.size());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());
 
-  index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1");
+  DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1");
   ASSERT_EQ(1u, s.size());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());
 
-  index_->LookupIdentifier(s, DICOM_TAG_SERIES_INSTANCE_UID, "1");
+  DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "1");
   ASSERT_EQ(0u, s.size());
 
-  /*{
-    std::list<std::string> s;
-    context.GetIndex().LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059");
-    for (std::list<std::string>::iterator i = s.begin(); i != s.end(); i++)
-    {
-    std::cout << "*** " << *i << std::endl;;
-    }      
-    }*/
+  {
+    LookupIdentifierQuery query(ResourceType_Study);
+    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0");
+    query.Apply(s, *index_);
+    ASSERT_EQ(3u, s.size());
+  }
+
+  {
+    LookupIdentifierQuery query(ResourceType_Study);
+    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0");
+    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "0");
+    query.Apply(s, *index_);
+    ASSERT_EQ(2u, s.size());
+  }
+
+  {
+    LookupIdentifierQuery query(ResourceType_Study);
+    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1");
+    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "1");
+    query.Apply(s, *index_);
+    ASSERT_EQ(1u, s.size());
+  }
+
+  {
+    LookupIdentifierQuery query(ResourceType_Study);
+    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1");
+    query.Apply(s, *index_);
+    ASSERT_EQ(1u, s.size());
+  }
+
+  {
+    LookupIdentifierQuery query(ResourceType_Study);
+    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "2");
+    query.Apply(s, *index_);
+    ASSERT_EQ(0u, s.size());
+  }
 }