changeset 614:f27923072afd find-move-scp

merge
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 24 Oct 2013 10:33:11 +0200
parents 60d90e48e809 (diff) b1a0990ad40c (current diff)
children ec0b7a51d7bd
files OrthancServer/main.cpp
diffstat 24 files changed, 999 insertions(+), 98 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Oct 24 10:32:33 2013 +0200
+++ b/CMakeLists.txt	Thu Oct 24 10:33:11 2013 +0200
@@ -90,6 +90,7 @@
 # Prepare the embedded files
 set(EMBEDDED_FILES
   PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql
+  PREPARE_DATABASE_V4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabaseV4.sql
   CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json
   LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua
   )
@@ -209,6 +210,7 @@
   OrthancServer/ServerContext.cpp
   OrthancServer/ServerEnumerations.cpp
   OrthancServer/ServerToolbox.cpp
+  OrthancServer/OrthancFindRequestHandler.cpp
   )
 
 # Ensure autogenerated code is built before building ServerLibrary
--- a/Core/DicomFormat/DicomMap.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/Core/DicomFormat/DicomMap.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -280,4 +280,110 @@
       SetValue(tag, source.GetValue(tag));
     }
   }
+
+
+  bool DicomMap::IsMainDicomTag(const DicomTag& tag, ResourceType level)
+  {
+    DicomTag *tags = NULL;
+    size_t size;
+
+    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);
+    }
+
+    for (size_t i = 0; i < size; i++)
+    {
+      if (tags[i] == tag)
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  bool DicomMap::IsMainDicomTag(const DicomTag& tag)
+  {
+    return (IsMainDicomTag(tag, ResourceType_Patient) ||
+            IsMainDicomTag(tag, ResourceType_Study) ||
+            IsMainDicomTag(tag, ResourceType_Series) ||
+            IsMainDicomTag(tag, ResourceType_Instance));
+  }
+
+
+  void DicomMap::GetMainDicomTagsInternal(std::set<DicomTag>& result, ResourceType level)
+  {
+    DicomTag *tags = NULL;
+    size_t size;
+
+    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);
+    }
+
+    for (size_t i = 0; i < size; i++)
+    {
+      result.insert(tags[i]);
+    }
+  }
+
+
+  void DicomMap::GetMainDicomTags(std::set<DicomTag>& result, ResourceType level)
+  {
+    result.clear();
+    GetMainDicomTagsInternal(result, level);
+  }
+
+
+  void DicomMap::GetMainDicomTags(std::set<DicomTag>& result)
+  {
+    result.clear();
+    GetMainDicomTagsInternal(result, ResourceType_Patient);
+    GetMainDicomTagsInternal(result, ResourceType_Study);
+    GetMainDicomTagsInternal(result, ResourceType_Series);
+    GetMainDicomTagsInternal(result, ResourceType_Instance);
+  }
 }
--- a/Core/DicomFormat/DicomMap.h	Thu Oct 24 10:32:33 2013 +0200
+++ b/Core/DicomFormat/DicomMap.h	Thu Oct 24 10:33:11 2013 +0200
@@ -35,7 +35,9 @@
 #include "DicomTag.h"
 #include "DicomValue.h"
 #include "DicomString.h"
+#include "../Enumerations.h"
 
+#include <set>
 #include <map>
 #include <json/json.h>
 
@@ -63,6 +65,8 @@
     void ExtractTags(DicomMap& source,
                      const DicomTag* tags,
                      size_t count) const;
+   
+    static void GetMainDicomTagsInternal(std::set<DicomTag>& result, ResourceType level);
 
   public:
     DicomMap()
@@ -148,5 +152,13 @@
 
     void CopyTagIfExists(const DicomMap& source,
                          const DicomTag& tag);
+
+    static bool IsMainDicomTag(const DicomTag& tag, ResourceType level);
+
+    static bool IsMainDicomTag(const DicomTag& tag);
+
+    static void GetMainDicomTags(std::set<DicomTag>& result, ResourceType level);
+
+    static void GetMainDicomTags(std::set<DicomTag>& result);
   };
 }
--- a/Core/DicomFormat/DicomTag.h	Thu Oct 24 10:32:33 2013 +0200
+++ b/Core/DicomFormat/DicomTag.h	Thu Oct 24 10:33:11 2013 +0200
@@ -110,4 +110,9 @@
   // DICOM tags used for fMRI (thanks to Will Ryder)
   static const DicomTag DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS(0x0020, 0x0105);
   static const DicomTag DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER(0x0020, 0x0100);
+
+  // Tags for C-FIND and C-MOVE
+  static const DicomTag DICOM_TAG_SPECIFIC_CHARACTER_SET(0x0008, 0x0005);
+  static const DicomTag DICOM_TAG_QUERY_RETRIEVE_LEVEL(0x0008, 0x0052);
+  static const DicomTag DICOM_TAG_MODALITIES_IN_STUDY(0x0008, 0x0061);
 }
--- a/Core/Enumerations.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/Core/Enumerations.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -33,6 +33,7 @@
 #include "Enumerations.h"
 
 #include "OrthancException.h"
+#include "Toolbox.h"
 
 namespace Orthanc
 {
@@ -222,4 +223,54 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
+
+
+  const char* EnumerationToString(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return "Patient";
+
+      case ResourceType_Study:
+        return "Study";
+
+      case ResourceType_Series:
+        return "Series";
+
+      case ResourceType_Instance:
+        return "Instance";
+      
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  ResourceType StringToResourceType(const char* type)
+  {
+    std::string s(type);
+    Toolbox::ToUpperCase(s);
+
+    if (s == "PATIENT")
+    {
+      return ResourceType_Patient;
+    }
+    else if (s == "STUDY")
+    {
+      return ResourceType_Study;
+    }
+    else if (s == "SERIES")
+    {
+      return ResourceType_Series;
+    }
+    else if (s == "INSTANCE" || s == "IMAGE")
+    {
+      return ResourceType_Instance;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
 }
--- a/Core/Enumerations.h	Thu Oct 24 10:32:33 2013 +0200
+++ b/Core/Enumerations.h	Thu Oct 24 10:33:11 2013 +0200
@@ -228,9 +228,20 @@
     FileContentType_Json = 2
   };
 
+  enum ResourceType
+  {
+    ResourceType_Patient = 1,
+    ResourceType_Study = 2,
+    ResourceType_Series = 3,
+    ResourceType_Instance = 4
+  };
 
 
   const char* EnumerationToString(HttpMethod method);
 
   const char* EnumerationToString(HttpStatus status);
+
+  const char* EnumerationToString(ResourceType type);
+
+  ResourceType StringToResourceType(const char* type);
 }
--- a/Core/Toolbox.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/Core/Toolbox.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -42,6 +42,7 @@
 #include <boost/uuid/sha1.hpp>
 #include <algorithm>
 #include <ctype.h>
+#include <boost/regex.hpp> 
 
 #if defined(_WIN32)
 #include <windows.h>
@@ -726,4 +727,60 @@
         throw OrthancException(ErrorCode_NotImplemented);
     }
   }
+
+
+  std::string Toolbox::WildcardToRegularExpression(const std::string& source)
+  {
+    // TODO - Speed up this with a regular expression
+
+    std::string result = source;
+
+    // Escape all special characters
+    boost::replace_all(result, "\\", "\\\\");
+    boost::replace_all(result, "^", "\\^");
+    boost::replace_all(result, ".", "\\.");
+    boost::replace_all(result, "$", "\\$");
+    boost::replace_all(result, "|", "\\|");
+    boost::replace_all(result, "(", "\\(");
+    boost::replace_all(result, ")", "\\)");
+    boost::replace_all(result, "[", "\\[");
+    boost::replace_all(result, "]", "\\]");
+    boost::replace_all(result, "+", "\\+");
+    boost::replace_all(result, "/", "\\/");
+    boost::replace_all(result, "{", "\\{");
+    boost::replace_all(result, "}", "\\}");
+
+    // Convert wildcards '*' and '?' to their regex equivalents
+    boost::replace_all(result, "?", ".");
+    boost::replace_all(result, "*", ".*");
+
+    return result;
+  }
+
+
+
+  void Toolbox::TokenizeString(std::vector<std::string>& result,
+                               const std::string& value,
+                               char separator)
+  {
+    result.clear();
+
+    std::string currentItem;
+
+    for (size_t i = 0; i < value.size(); i++)
+    {
+      if (value[i] == separator)
+      {
+        result.push_back(currentItem);
+        currentItem.clear();
+      }
+      else
+      {
+        currentItem.push_back(value[i]);
+      }
+    }
+
+    result.push_back(currentItem);
+  }
 }
+
--- a/Core/Toolbox.h	Thu Oct 24 10:32:33 2013 +0200
+++ b/Core/Toolbox.h	Thu Oct 24 10:33:11 2013 +0200
@@ -108,5 +108,11 @@
     void UrlDecode(std::string& s);
 
     Endianness DetectEndianness();
+
+    std::string WildcardToRegularExpression(const std::string& s);
+
+    void TokenizeString(std::vector<std::string>& result,
+                        const std::string& source,
+                        char separator);
   }
 }
--- a/OrthancServer/DatabaseWrapper.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/DatabaseWrapper.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -807,7 +807,7 @@
       db_.Execute(query);
     }
 
-    // Sanity check of the version of the database
+    // Check the version of the database
     std::string version = GetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "Unknown");
     bool ok = false;
     try
@@ -815,9 +815,27 @@
       LOG(INFO) << "Version of the Orthanc database: " << version;
       unsigned int v = boost::lexical_cast<unsigned int>(version);
 
-      // This version of Orthanc is only compatible with version 3 of
-      // the DB schema (since Orthanc 0.3.2)
-      ok = (v == 3); 
+      // This version of Orthanc is only compatible with versions 3
+      // (Orthanc 0.3.2 to 0.6.1) and 4 (since Orthanc 0.6.2) of the
+      // DB schema
+      ok = (v == 3 || v == 4);
+
+      if (v == 3)
+      {
+        LOG(WARNING) << "Upgrading the database from version 3 to version 4 (reconstructing the index)";
+
+        // Reconstruct the index for case insensitive queries in C-FIND
+        db_.Execute("DROP INDEX IF EXISTS MainDicomTagsIndexValues;");
+        db_.Execute("DROP TABLE IF EXISTS AvailableTags;");
+
+        std::string query;
+        EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE_V4);
+        db_.Execute(query);
+
+        db_.Execute("INSERT INTO AvailableTags SELECT DISTINCT tagGroup, tagElement FROM MainDicomTags;");
+
+        //SetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "4");
+      }
     }
     catch (boost::bad_lexical_cast&)
     {
@@ -828,6 +846,8 @@
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
     }
 
+    CompleteMainDicomTags();
+
     signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
     db_.Register(signalRemainingAncestor_);
     db_.Register(new Internals::SignalFileDeleted(listener_));
@@ -995,4 +1015,11 @@
       result.push_back(s.ColumnInt64(0));
     }
   }
+
+
+  void DatabaseWrapper::CompleteMainDicomTags()
+  {
+    std::set<DicomTag> requiredTags;
+    
+  }
 }
--- a/OrthancServer/DatabaseWrapper.h	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/DatabaseWrapper.h	Thu Oct 24 10:33:11 2013 +0200
@@ -72,6 +72,8 @@
                               int64_t since,
                               unsigned int maxResults);
 
+    void CompleteMainDicomTags();
+
   public:
     void SetGlobalProperty(GlobalProperty property,
                            const std::string& value);
--- a/OrthancServer/Internals/MoveScp.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/Internals/MoveScp.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -82,6 +82,13 @@
         try
         {
           data.iterator_.reset(data.handler_->Handle(data.target_, data.input_));
+          if (data.iterator_.get() == NULL)
+          {
+            // Internal error!
+            response->DimseStatus = STATUS_MOVE_Failed_UnableToProcess;
+            return;
+          }
+
           data.subOperationCount_ = data.iterator_->GetSubOperationCount();
           data.failureCount_ = 0;
           data.warningCount_ = 0;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -0,0 +1,362 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2013 Medical Physics Department, CHU 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 "OrthancFindRequestHandler.h"
+
+#include <glog/logging.h>
+#include <boost/regex.hpp> 
+
+#include "../Core/DicomFormat/DicomArray.h"
+#include "ServerToolbox.h"
+
+namespace Orthanc
+{
+  static std::string ToLowerCase(const std::string& s)
+  {
+    std::string result = s;
+    Toolbox::ToLowerCase(result);
+    return result;
+  }
+
+  static bool ApplyRangeConstraint(const std::string& value,
+                                   const std::string& constraint)
+  {
+    size_t separator = constraint.find('-');
+    std::string lower = ToLowerCase(constraint.substr(0, separator));
+    std::string upper = ToLowerCase(constraint.substr(separator + 1));
+    std::string v = ToLowerCase(value);
+
+    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);
+  }
+
+
+  static bool ApplyListConstraint(const std::string& value,
+                                  const std::string& constraint)
+  {
+    std::string v1 = ToLowerCase(value);
+
+    std::vector<std::string> items;
+    Toolbox::TokenizeString(items, constraint, '\\');
+
+    for (size_t i = 0; i < items.size(); i++)
+    {
+      Toolbox::ToLowerCase(items[i]);
+      if (items[i] == v1)
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  static bool Matches(const std::string& value,
+                      const std::string& constraint)
+  {
+    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
+    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
+
+    if (constraint.find('-') != std::string::npos)
+    {
+      return ApplyRangeConstraint(value, constraint);
+    }
+    
+    if (constraint.find('\\') != std::string::npos)
+    {
+      return ApplyListConstraint(value, constraint);
+    }
+
+    if (constraint.find('*') != std::string::npos ||
+        constraint.find('?') != std::string::npos)
+    {
+      // TODO - Cache the constructed regular expression
+      boost::regex pattern(Toolbox::WildcardToRegularExpression(constraint),
+                           boost::regex::icase /* case insensitive search */);
+      return boost::regex_match(value, pattern);
+    }
+    else
+    {
+      return ToLowerCase(value) == ToLowerCase(constraint);
+    }
+  }
+
+
+  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.size() == 0)
+      {
+        return false;
+      }
+
+      childId = children.front();
+    }
+
+    return LookupOneInstance(result, index, childId, GetChildResourceType(type));
+  }
+
+
+  static bool Matches(const Json::Value& resource,
+                      const DicomArray& query)
+  {
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      if (query.GetElement(i).GetValue().IsNull() ||
+          query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
+          query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET ||
+          query.GetElement(i).GetTag() == DICOM_TAG_MODALITIES_IN_STUDY)
+      {
+        continue;
+      }
+
+      std::string tag = query.GetElement(i).GetTag().Format();
+      std::string value;
+      if (resource.isMember(tag))
+      {
+        value = resource.get(tag, Json::arrayValue).get("Value", "").asString();
+      }
+
+      if (!Matches(value, query.GetElement(i).GetValue().AsString()))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  static void AddAnswer(DicomFindAnswers& answers,
+                        const Json::Value& resource,
+                        const DicomArray& query)
+  {
+    DicomMap result;
+
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      if (query.GetElement(i).GetTag() != DICOM_TAG_QUERY_RETRIEVE_LEVEL &&
+          query.GetElement(i).GetTag() != DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        std::string tag = query.GetElement(i).GetTag().Format();
+        std::string value;
+        if (resource.isMember(tag))
+        {
+          value = resource.get(tag, Json::arrayValue).get("Value", "").asString();
+          result.SetValue(query.GetElement(i).GetTag(), value);
+        }
+      }
+    }
+
+    answers.Add(result);
+  }
+
+
+  static bool ApplyModalitiesInStudyFilter(Json::Value& filteredStudies,
+                                           const Json::Value& studies,
+                                           const DicomMap& input,
+                                           ServerIndex& index)
+  {
+    filteredStudies = Json::arrayValue;
+
+    const DicomValue& v = input.GetValue(DICOM_TAG_MODALITIES_IN_STUDY);
+    if (v.IsNull())
+    {
+      return false;
+    }
+
+    // Move the allowed modalities into a "std::set"
+    std::vector<std::string>  tmp;
+    Toolbox::TokenizeString(tmp, v.AsString(), '\\'); 
+
+    std::set<std::string> modalities;
+    for (size_t i = 0; i < tmp.size(); i++)
+    {
+      modalities.insert(tmp[i]);
+    }
+
+    // Loop over the studies
+    for (Json::Value::ArrayIndex i = 0; i < studies.size(); i++)
+    {
+      try
+      {
+        // We are considering a single study. Check whether one of
+        // its child series matches one of the modalities.
+        Json::Value study;
+        if (index.LookupResource(study, studies[i].asString(), 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 (modalities.find(modality) != modalities.end())
+                {
+                  // This series of the considered study matches one
+                  // of the required modalities. Take the study into
+                  // consideration for future filtering.
+                  filteredStudies.append(studies[i]);
+
+                  // We have finished considering this study. Break the study loop at (*).
+                  break;
+                }
+              }
+            }
+          }
+        }
+      }
+      catch (OrthancException&)
+      {
+        // This resource has probably been deleted during the find request
+      }
+    }
+
+    return true;
+  }
+
+
+  void OrthancFindRequestHandler::Handle(const DicomMap& input,
+                                         DicomFindAnswers& answers)
+  {
+    LOG(WARNING) << "Find-SCU request received";
+
+    /**
+     * Retrieve the query level.
+     **/
+
+    const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    if (levelTmp == NULL) 
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    ResourceType level = StringToResourceType(levelTmp->AsString().c_str());
+
+    if (level != ResourceType_Patient &&
+        level != ResourceType_Study &&
+        level != ResourceType_Series)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+
+    /**
+     * Retrieve all the resources for this query level.
+     **/
+
+    Json::Value resources;
+    context_.GetIndex().GetAllUuids(resources, level);
+    assert(resources.type() == Json::arrayValue);
+
+    // TODO : Speed up using MainDicomTags (to avoid looping over ALL
+    // the resources and reading the JSON file for each of them)
+
+
+
+    /**
+     * Apply filtering on modalities for studies, if asked (this is an
+     * extension to standard DICOM)
+     * http://www.medicalconnections.co.uk/kb/Filtering_on_and_Retrieving_the_Modality_in_a_C_FIND
+     **/
+
+    if (level == ResourceType_Study &&
+        input.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
+    {
+      Json::Value filtered;
+      if (ApplyModalitiesInStudyFilter(filtered, resources, input, context_.GetIndex()))
+      {
+        resources = filtered;
+      }
+    }
+
+
+    /**
+     * Loop over all the resources for this query level.
+     **/
+
+    DicomArray query(input);
+    for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
+    {
+      try
+      {
+        std::string instance;
+        if (LookupOneInstance(instance, context_.GetIndex(), resources[i].asString(), level))
+        {
+          Json::Value resource;
+          context_.ReadJson(resource, instance);
+        
+          if (Matches(resource, query))
+          {
+            AddAnswer(answers, resource, query);
+          }
+        }
+      }
+      catch (OrthancException&)
+      {
+        // This resource has probably been deleted during the find request
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancFindRequestHandler.h	Thu Oct 24 10:33:11 2013 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2013 Medical Physics Department, CHU 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 "DicomProtocol/IFindRequestHandler.h"
+
+#include "ServerContext.h"
+
+namespace Orthanc
+{
+  class OrthancFindRequestHandler : public IFindRequestHandler
+  {
+  private:
+    ServerContext& context_;
+
+  public:
+    OrthancFindRequestHandler(ServerContext& context) :
+    context_(context)
+    {
+    }
+
+    virtual void Handle(const DicomMap& input,
+                        DicomFindAnswers& answers);
+  };
+}
--- a/OrthancServer/OrthancInitialization.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/OrthancInitialization.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -464,4 +464,69 @@
       target.push_back(lst[i].asString());
     }    
   }
+
+
+  void ConnectToModalityUsingSymbolicName(DicomUserConnection& connection,
+                                          const std::string& name)
+  {
+    std::string aet, address;
+    int port;
+    ModalityManufacturer manufacturer;
+    GetDicomModality(name, aet, address, port, manufacturer);
+
+    LOG(WARNING) << "Connecting to remote DICOM modality: AET=" << aet << ", address=" << address << ", port=" << port;
+
+    connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC"));
+    connection.SetDistantApplicationEntityTitle(aet);
+    connection.SetDistantHost(address);
+    connection.SetDistantPort(port);
+    connection.SetDistantManufacturer(manufacturer);
+    connection.Open();
+  }
+
+
+  void ConnectToModalityUsingAETitle(DicomUserConnection& connection,
+                                     const std::string& aet)
+  {
+    std::set<std::string> modalities;
+    GetListOfDicomModalities(modalities);
+
+    std::string address;
+    int port;
+    ModalityManufacturer manufacturer;
+    bool found = false;
+
+    for (std::set<std::string>::const_iterator 
+           it = modalities.begin(); it != modalities.end(); it++)
+    {
+      try
+      {
+        std::string thisAet;
+        GetDicomModality(*it, thisAet, address, port, manufacturer);
+        
+        if (aet == thisAet)
+        {
+          found = true;
+          break;
+        }
+      }
+      catch (OrthancException&)
+      {
+      }
+    }
+
+    if (!found)
+    {
+      throw OrthancException("Unknown modality: " + aet);
+    }
+
+    LOG(WARNING) << "Connecting to remote DICOM modality: AET=" << aet << ", address=" << address << ", port=" << port;
+
+    connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC"));
+    connection.SetDistantApplicationEntityTitle(aet);
+    connection.SetDistantHost(address);
+    connection.SetDistantPort(port);
+    connection.SetDistantManufacturer(manufacturer);
+    connection.Open();
+  }
 }
--- a/OrthancServer/OrthancInitialization.h	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/OrthancInitialization.h	Thu Oct 24 10:33:11 2013 +0200
@@ -37,6 +37,7 @@
 #include <json/json.h>
 #include <stdint.h>
 #include "../Core/HttpServer/MongooseServer.h"
+#include "DicomProtocol/DicomUserConnection.h"
 #include "ServerEnumerations.h"
 
 namespace Orthanc
@@ -78,4 +79,10 @@
 
   void GetGlobalListOfStringsParameter(std::list<std::string>& target,
                                        const std::string& key);
+
+  void ConnectToModalityUsingSymbolicName(DicomUserConnection& connection,
+                                          const std::string& name);
+
+  void ConnectToModalityUsingAETitle(DicomUserConnection& connection,
+                                     const std::string& aet);
 }
--- a/OrthancServer/OrthancRestApi.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/OrthancRestApi.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -71,21 +71,6 @@
 
   // DICOM SCU ----------------------------------------------------------------
 
-  static void ConnectToModality(DicomUserConnection& connection,
-                                const std::string& name)
-  {
-    std::string aet, address;
-    int port;
-    ModalityManufacturer manufacturer;
-    GetDicomModality(name, aet, address, port, manufacturer);
-    connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC"));
-    connection.SetDistantApplicationEntityTitle(aet);
-    connection.SetDistantHost(address);
-    connection.SetDistantPort(port);
-    connection.SetDistantManufacturer(manufacturer);
-    connection.Open();
-  }
-
   static bool MergeQueryAndTemplate(DicomMap& result,
                                     const std::string& postData)
   {
@@ -118,7 +103,7 @@
     }
 
     DicomUserConnection connection;
-    ConnectToModality(connection, call.GetUriComponent("id", ""));
+    ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
 
     DicomFindAnswers answers;
     connection.FindPatient(answers, m);
@@ -144,7 +129,7 @@
     }        
       
     DicomUserConnection connection;
-    ConnectToModality(connection, call.GetUriComponent("id", ""));
+    ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
   
     DicomFindAnswers answers;
     connection.FindStudy(answers, m);
@@ -171,7 +156,7 @@
     }        
          
     DicomUserConnection connection;
-    ConnectToModality(connection, call.GetUriComponent("id", ""));
+    ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
   
     DicomFindAnswers answers;
     connection.FindSeries(answers, m);
@@ -199,7 +184,7 @@
     }        
          
     DicomUserConnection connection;
-    ConnectToModality(connection, call.GetUriComponent("id", ""));
+    ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
   
     DicomFindAnswers answers;
     connection.FindInstance(answers, m);
@@ -219,7 +204,7 @@
     }
  
     DicomUserConnection connection;
-    ConnectToModality(connection, call.GetUriComponent("id", ""));
+    ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
   
     DicomFindAnswers patients;
     connection.FindPatient(patients, m);
@@ -350,7 +335,7 @@
     }
 
     DicomUserConnection connection;
-    ConnectToModality(connection, remote);
+    ConnectToModalityUsingSymbolicName(connection, remote);
 
     for (std::list<std::string>::const_iterator 
            it = instances.begin(); it != instances.end(); it++)
--- a/OrthancServer/PrepareDatabase.sql	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/PrepareDatabase.sql	Thu Oct 24 10:33:11 2013 +0200
@@ -67,7 +67,6 @@
 
 CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id);
 CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
-CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY);
 
 CREATE INDEX ChangesIndex ON Changes(internalId);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/PrepareDatabaseV4.sql	Thu Oct 24 10:33:11 2013 +0200
@@ -0,0 +1,12 @@
+-- New in database version 4
+CREATE TABLE AvailableTags(
+       tagGroup INTEGER,
+       tagElement INTEGER,
+       PRIMARY KEY(tagGroup, tagElement)
+       );
+
+-- Until database version 4, the following index was set to "COLLATE
+-- BINARY". This implies case-sensitive searches, but DICOM C-Find
+-- requires case-insensitive searches.
+-- http://www.sqlite.org/optoverview.html#like_opt
+CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE NOCASE);
--- a/OrthancServer/ServerEnumerations.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/ServerEnumerations.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -33,6 +33,7 @@
 
 #include "../Core/OrthancException.h"
 #include "../Core/EnumerationDictionary.h"
+#include "../Core/Toolbox.h"
 
 #include <boost/thread.hpp>
 
@@ -82,27 +83,6 @@
     return dictMetadataType_.Translate(str);
   }
 
-  const char* EnumerationToString(ResourceType type)
-  {
-    switch (type)
-    {
-      case ResourceType_Patient:
-        return "Patient";
-
-      case ResourceType_Study:
-        return "Study";
-
-      case ResourceType_Series:
-        return "Series";
-
-      case ResourceType_Instance:
-        return "Instance";
-      
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
   std::string GetBasePath(ResourceType type,
                           const std::string& publicId)
   {
@@ -289,6 +269,4 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
-
-
 }
--- a/OrthancServer/ServerEnumerations.h	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/ServerEnumerations.h	Thu Oct 24 10:33:11 2013 +0200
@@ -33,6 +33,8 @@
 
 #include <string>
 
+#include "../Core/Enumerations.h"
+
 namespace Orthanc
 {
   enum SeriesStatus
@@ -71,14 +73,6 @@
     GlobalProperty_AnonymizationSequence = 3
   };
 
-  enum ResourceType
-  {
-    ResourceType_Patient = 1,
-    ResourceType_Study = 2,
-    ResourceType_Series = 3,
-    ResourceType_Instance = 4
-  };
-
   enum MetadataType
   {
     MetadataType_Instance_IndexInSeries = 1,
@@ -122,8 +116,6 @@
 
   MetadataType StringToMetadata(const std::string& str);
 
-  const char* EnumerationToString(ResourceType type);
-
   std::string EnumerationToString(MetadataType type);
 
   const char* EnumerationToString(SeriesStatus status);
--- a/OrthancServer/main.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/OrthancServer/main.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -41,20 +41,22 @@
 #include "../Core/Lua/LuaFunctionCall.h"
 #include "../Core/DicomFormat/DicomArray.h"
 #include "DicomProtocol/DicomServer.h"
+#include "DicomProtocol/DicomUserConnection.h"
 #include "OrthancInitialization.h"
 #include "ServerContext.h"
+#include "OrthancFindRequestHandler.h"
 
 using namespace Orthanc;
 
 
 
-class MyStoreRequestHandler : public IStoreRequestHandler
+class OrthancStoreRequestHandler : public IStoreRequestHandler
 {
 private:
   ServerContext& server_;
 
 public:
-  MyStoreRequestHandler(ServerContext& context) :
+  OrthancStoreRequestHandler(ServerContext& context) :
     server_(context)
   {
   }
@@ -72,34 +74,92 @@
 };
 
 
-class MyFindRequestHandler : public IFindRequestHandler
+
+class OrthancMoveRequestIterator : public IMoveRequestIterator
+{
+private:
+  ServerContext& context_;
+  std::vector<std::string> instances_;
+  DicomUserConnection connection_;
+  size_t position_;
+
+public:
+  OrthancMoveRequestIterator(ServerContext& context,
+                             const std::string& target,
+                             const std::string& publicId) :
+    context_(context),
+    position_(0)
+  {
+    LOG(INFO) << "Sending resource " << publicId << " to modality \"" << target << "\"";
+
+    std::list<std::string> tmp;
+    context_.GetIndex().GetChildInstances(tmp, publicId);
+
+    instances_.reserve(tmp.size());
+    for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); it++)
+    {
+      instances_.push_back(*it);
+    }
+    
+    ConnectToModalityUsingAETitle(connection_, target);
+  }
+
+  virtual unsigned int GetSubOperationCount() const
+  {
+    return instances_.size();
+  }
+
+  virtual Status DoNext()
+  {
+    if (position_ >= instances_.size())
+    {
+      return Status_Failure;
+    }
+
+    const std::string& id = instances_[position_++];
+
+    std::string dicom;
+    context_.ReadFile(dicom, id, FileContentType_Dicom);
+    connection_.Store(dicom);
+
+    return Status_Success;
+  }
+};
+
+
+
+class OrthancMoveRequestHandler : public IMoveRequestHandler
 {
 private:
   ServerContext& context_;
 
-public:
-  MyFindRequestHandler(ServerContext& context) :
-    context_(context)
+  bool LookupResource(std::string& publicId,
+                      DicomTag tag,
+                      const DicomMap& input)
   {
+    if (!input.HasTag(tag))
+    {
+      return false;
+    }
+
+    std::string value = input.GetValue(tag).AsString();
+
+    std::list<std::string> ids;
+    context_.GetIndex().LookupTagValue(ids, tag, value);
+
+    if (ids.size() != 1)
+    {
+      return false;
+    }
+    else
+    {
+      publicId = ids.front();
+      return true;
+    }
   }
 
-  virtual void Handle(const DicomMap& input,
-                      DicomFindAnswers& answers)
-  {
-    LOG(WARNING) << "Find-SCU request received";
-    DicomArray a(input);
-    a.Print(stdout);
-  }
-};
-
-
-class MyMoveRequestHandler : public IMoveRequestHandler
-{
-private:
-  ServerContext& context_;
-
 public:
-  MyMoveRequestHandler(ServerContext& context) :
+  OrthancMoveRequestHandler(ServerContext& context) :
     context_(context)
   {
   }
@@ -108,8 +168,57 @@
   virtual IMoveRequestIterator* Handle(const std::string& target,
                                        const DicomMap& input)
   {
-    LOG(WARNING) << "Move-SCU request received";
-    return NULL;
+    LOG(WARNING) << "Move-SCU request received for AET \"" << target << "\"";
+
+
+    /**
+     * Retrieve the query level.
+     **/
+
+    const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    if (levelTmp == NULL) 
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    ResourceType level = StringToResourceType(levelTmp->AsString().c_str());
+
+
+    /**
+     * Lookup for the resource to be sent.
+     **/
+
+    bool ok;
+    std::string publicId;
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        ok = LookupResource(publicId, DICOM_TAG_PATIENT_ID, input);
+        break;
+
+      case ResourceType_Study:
+        ok = LookupResource(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input);
+        break;
+
+      case ResourceType_Series:
+        ok = LookupResource(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input);
+        break;
+
+      case ResourceType_Instance:
+        ok = LookupResource(publicId, DICOM_TAG_SOP_INSTANCE_UID, input);
+        break;
+
+      default:
+        ok = false;
+    }
+
+    if (!ok)
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    return new OrthancMoveRequestIterator(context_, target, publicId);
   }
 };
 
@@ -129,17 +238,17 @@
 
   virtual IStoreRequestHandler* ConstructStoreRequestHandler()
   {
-    return new MyStoreRequestHandler(context_);
+    return new OrthancStoreRequestHandler(context_);
   }
 
   virtual IFindRequestHandler* ConstructFindRequestHandler()
   {
-    return new MyFindRequestHandler(context_);
+    return new OrthancFindRequestHandler(context_);
   }
 
   virtual IMoveRequestHandler* ConstructMoveRequestHandler()
   {
-    return new MyMoveRequestHandler(context_);
+    return new OrthancMoveRequestHandler(context_);
   }
 
   void Done()
@@ -369,14 +478,13 @@
 
     MyDicomServerFactory serverFactory(context);
     
-
     {
       // DICOM server
       DicomServer dicomServer;
       dicomServer.SetCalledApplicationEntityTitleCheck(GetGlobalBoolParameter("DicomCheckCalledAet", false));
       dicomServer.SetStoreRequestHandlerFactory(serverFactory);
-      //dicomServer.SetMoveRequestHandlerFactory(serverFactory);
-      //dicomServer.SetFindRequestHandlerFactory(serverFactory);
+      dicomServer.SetMoveRequestHandlerFactory(serverFactory);
+      dicomServer.SetFindRequestHandlerFactory(serverFactory);
       dicomServer.SetPortNumber(GetGlobalIntegerParameter("DicomPort", 4242));
       dicomServer.SetApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC"));
 
--- a/Resources/CMake/BoostConfiguration.cmake	Thu Oct 24 10:32:33 2013 +0200
+++ b/Resources/CMake/BoostConfiguration.cmake	Thu Oct 24 10:33:11 2013 +0200
@@ -1,14 +1,14 @@
 if (${STATIC_BUILD})
-  SET(BOOST_STATIC 1)
+  set(BOOST_STATIC 1)
 else()
   include(FindBoost)
 
-  SET(BOOST_STATIC 0)
+  set(BOOST_STATIC 0)
   #set(Boost_DEBUG 1)
   #set(Boost_USE_STATIC_LIBS ON)
 
   find_package(Boost
-    COMPONENTS filesystem thread system date_time)
+    COMPONENTS filesystem thread system date_time regex)
 
   if (NOT Boost_FOUND)
     message(FATAL_ERROR "Unable to locate Boost on this system")
@@ -30,7 +30,7 @@
   #if (${Boost_VERSION} LESS 104800)
   # boost::locale is only available from 1.48.00
   #message("Too old version of Boost (${Boost_LIB_VERSION}): Building the static version")
-  #  SET(BOOST_STATIC 1)
+  #  set(BOOST_STATIC 1)
   #endif()
 
   include_directories(${Boost_INCLUDE_DIRS})
@@ -40,12 +40,12 @@
 
 if (BOOST_STATIC)
   # Parameters for Boost 1.54.0
-  SET(BOOST_NAME boost_1_54_0)
-  SET(BOOST_BCP_SUFFIX bcpdigest-0.6.2)
-  SET(BOOST_MD5 "a464288a976ba133f9b325f454cb503d")
-  SET(BOOST_FILESYSTEM_SOURCES_DIR "${BOOST_NAME}/libs/filesystem/src")
+  set(BOOST_NAME boost_1_54_0)
+  set(BOOST_BCP_SUFFIX bcpdigest-0.6.2)
+  set(BOOST_MD5 "a464288a976ba133f9b325f454cb503d")
+  set(BOOST_FILESYSTEM_SOURCES_DIR "${BOOST_NAME}/libs/filesystem/src")
   
-  SET(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME})
+  set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME})
   DownloadPackage(
     "${BOOST_MD5}"
     "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz"
@@ -80,7 +80,10 @@
     message(FATAL_ERROR "Support your platform here")
   endif()
 
+  aux_source_directory(${BOOST_SOURCES_DIR}/libs/regex/src BOOST_REGEX_SOURCES)
+
   list(APPEND BOOST_SOURCES
+    ${BOOST_REGEX_SOURCES}
     ${BOOST_SOURCES_DIR}/libs/date_time/src/gregorian/greg_month.cpp
     ${BOOST_FILESYSTEM_SOURCES_DIR}/codecvt_error_category.cpp
     ${BOOST_FILESYSTEM_SOURCES_DIR}/operations.cpp
--- a/UnitTests/ServerIndex.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/UnitTests/ServerIndex.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -487,3 +487,11 @@
 
 
 }
+
+
+TEST(DicomMap, MainTags)
+{
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID));
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient));
+  ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study));
+}
--- a/UnitTests/main.cpp	Thu Oct 24 10:32:33 2013 +0200
+++ b/UnitTests/main.cpp	Thu Oct 24 10:33:11 2013 +0200
@@ -127,6 +127,10 @@
   t = FromDcmtkBridge::ParseTag("0020-e040");
   ASSERT_EQ(0x0020, t.GetGroup());
   ASSERT_EQ(0xe040, t.GetElement());
+
+  // Test ==() and !=() operators
+  ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020));
+  ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020));
 }
 
 
@@ -387,6 +391,13 @@
   ASSERT_EQ("IndexInSeries", EnumerationToString(MetadataType_Instance_IndexInSeries));
   ASSERT_EQ("LastUpdate", EnumerationToString(MetadataType_LastUpdate));
 
+  ASSERT_EQ(ResourceType_Patient, StringToResourceType("PATienT"));
+  ASSERT_EQ(ResourceType_Study, StringToResourceType("STudy"));
+  ASSERT_EQ(ResourceType_Series, StringToResourceType("SeRiEs"));
+  ASSERT_EQ(ResourceType_Instance, StringToResourceType("INStance"));
+  ASSERT_EQ(ResourceType_Instance, StringToResourceType("IMagE"));
+  ASSERT_THROW(StringToResourceType("heLLo"), OrthancException);
+
   ASSERT_EQ(2047, StringToMetadata("2047"));
   ASSERT_THROW(StringToMetadata("Ceci est un test"), OrthancException);
   ASSERT_THROW(RegisterUserMetadata(128, ""), OrthancException); // too low (< 1024)
@@ -480,6 +491,37 @@
 }
 
 
+TEST(Toolbox, Wildcard)
+{
+  ASSERT_EQ("abcd", Toolbox::WildcardToRegularExpression("abcd"));
+  ASSERT_EQ("ab.*cd", Toolbox::WildcardToRegularExpression("ab*cd"));
+  ASSERT_EQ("ab..cd", Toolbox::WildcardToRegularExpression("ab??cd"));
+  ASSERT_EQ("a.*b.c.*d", Toolbox::WildcardToRegularExpression("a*b?c*d"));
+  ASSERT_EQ("a\\{b\\]", Toolbox::WildcardToRegularExpression("a{b]"));
+}
+
+
+TEST(Toolbox, Tokenize)
+{
+  std::vector<std::string> t;
+  
+  Toolbox::TokenizeString(t, "", ','); 
+  ASSERT_EQ(1, t.size());
+  ASSERT_EQ("", t[0]);
+  
+  Toolbox::TokenizeString(t, "abc", ','); 
+  ASSERT_EQ(1, t.size());
+  ASSERT_EQ("abc", t[0]);
+  
+  Toolbox::TokenizeString(t, "ab,cd,ef,", ','); 
+  ASSERT_EQ(4, t.size());
+  ASSERT_EQ("ab", t[0]);
+  ASSERT_EQ("cd", t[1]);
+  ASSERT_EQ("ef", t[2]);
+  ASSERT_EQ("", t[3]);
+}
+
+
 int main(int argc, char **argv)
 {
   // Initialize Google's logging library.