changeset 624:b58d65608949

integration find-move-scp -> mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 25 Oct 2013 12:42:38 +0200
parents 5651d2c6f6fd (current diff) 5c24273d54f9 (diff)
children 7e7f41a9f33d
files
diffstat 31 files changed, 1395 insertions(+), 178 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Fri Oct 25 10:36:06 2013 +0200
+++ b/CMakeLists.txt	Fri Oct 25 12:42:38 2013 +0200
@@ -209,6 +209,8 @@
   OrthancServer/ServerContext.cpp
   OrthancServer/ServerEnumerations.cpp
   OrthancServer/ServerToolbox.cpp
+  OrthancServer/OrthancFindRequestHandler.cpp
+  OrthancServer/OrthancMoveRequestHandler.cpp
   )
 
 # Ensure autogenerated code is built before building ServerLibrary
--- a/Core/DicomFormat/DicomMap.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/Core/DicomFormat/DicomMap.cpp	Fri Oct 25 12:42:38 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	Fri Oct 25 10:36:06 2013 +0200
+++ b/Core/DicomFormat/DicomMap.h	Fri Oct 25 12:42:38 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	Fri Oct 25 10:36:06 2013 +0200
+++ b/Core/DicomFormat/DicomTag.h	Fri Oct 25 12:42:38 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	Fri Oct 25 10:36:06 2013 +0200
+++ b/Core/Enumerations.cpp	Fri Oct 25 12:42:38 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	Fri Oct 25 10:36:06 2013 +0200
+++ b/Core/Enumerations.h	Fri Oct 25 12:42:38 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	Fri Oct 25 10:36:06 2013 +0200
+++ b/Core/Toolbox.cpp	Fri Oct 25 12:42:38 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	Fri Oct 25 10:36:06 2013 +0200
+++ b/Core/Toolbox.h	Fri Oct 25 12:42:38 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/NEWS	Fri Oct 25 10:36:06 2013 +0200
+++ b/NEWS	Fri Oct 25 12:42:38 2013 +0200
@@ -1,6 +1,7 @@
 Pending changes in the mainline
 ===============================
 
+* DICOM Query/Retrieve is supported
 * Possibility to keep the PatientID during an anonymization
 * Check whether "unzip", "tar" and/or "7-zip" are installed from CMake
 
--- a/OrthancServer/DatabaseWrapper.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/DatabaseWrapper.cpp	Fri Oct 25 12:42:38 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
@@ -817,7 +817,7 @@
 
       // This version of Orthanc is only compatible with version 3 of
       // the DB schema (since Orthanc 0.3.2)
-      ok = (v == 3); 
+      ok = (v == 3);
     }
     catch (boost::bad_lexical_cast&)
     {
--- a/OrthancServer/DicomProtocol/IApplicationEntityFilter.h	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/DicomProtocol/IApplicationEntityFilter.h	Fri Oct 25 12:42:38 2013 +0200
@@ -32,6 +32,8 @@
 
 #pragma once
 
+#include "../ServerEnumerations.h"
+
 #include <string>
 
 namespace Orthanc
@@ -43,7 +45,11 @@
     {
     }
 
-    virtual bool IsAllowed(const std::string& callingIp,
-                           const std::string& callingAet) = 0;
+    virtual bool IsAllowedConnection(const std::string& callingIp,
+                                     const std::string& callingAet) = 0;
+
+    virtual bool IsAllowedRequest(const std::string& callingIp,
+                                  const std::string& callingAet,
+                                  DicomRequestType type) = 0;
   };
 }
--- a/OrthancServer/DicomProtocol/IFindRequestHandler.h	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/DicomProtocol/IFindRequestHandler.h	Fri Oct 25 12:42:38 2013 +0200
@@ -47,7 +47,7 @@
     {
     }
 
-    virtual void Handle(const DicomMap& input,
-                        DicomFindAnswers& answers) = 0;
+    virtual void Handle(DicomFindAnswers& answers,
+                        const DicomMap& input) = 0;
   };
 }
--- a/OrthancServer/Internals/CommandDispatcher.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/Internals/CommandDispatcher.cpp	Fri Oct 25 12:42:38 2013 +0200
@@ -326,6 +326,9 @@
         AssociationCleanup(assoc);
         return NULL;
       }
+
+      std::string callingIP;
+      std::string callingTitle;
   
       /* check the AETs */
       {
@@ -347,8 +350,8 @@
           return NULL;
         }
 
-        std::string callingIP(/*OFSTRING_GUARD*/(callingIP_C));
-        std::string callingTitle(/*OFSTRING_GUARD*/(callingTitle_C));
+        callingIP = std::string(/*OFSTRING_GUARD*/(callingIP_C));
+        callingTitle = std::string(/*OFSTRING_GUARD*/(callingTitle_C));
         std::string calledTitle(/*OFSTRING_GUARD*/(calledTitle_C));
         Toolbox::ToUpperCase(callingIP);
         Toolbox::ToUpperCase(callingTitle);
@@ -369,7 +372,7 @@
         }
 
         if (server.HasApplicationEntityFilter() &&
-            !server.GetApplicationEntityFilter().IsAllowed(callingIP, callingTitle))
+            !server.GetApplicationEntityFilter().IsAllowedConnection(callingIP, callingTitle))
         {
           T_ASC_RejectParameters rej =
             {
@@ -416,7 +419,8 @@
           LOG(INFO) << "    (but no valid presentation contexts)";
       }
 
-      return new CommandDispatcher(server, assoc);
+      IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL;
+      return new CommandDispatcher(server, assoc, callingIP, callingTitle, filter);
     }
 
     bool CommandDispatcher::Step()
@@ -463,56 +467,94 @@
         // Reset the client timeout counter
         elapsedTimeSinceLastCommand_ = 0;
 
-        // in case we received a valid message, process this command
-        // note that storescp can only process a C-ECHO-RQ and a C-STORE-RQ
+        // Convert the type of request to Orthanc's internal type
+        bool supported = false;
+        DicomRequestType request;
         switch (msg.CommandField)
         {
-        case DIMSE_C_ECHO_RQ:
-          // process C-ECHO-Request
-          cond = EchoScp(assoc_, &msg, presID);
-          break;
+          case DIMSE_C_ECHO_RQ:
+            request = DicomRequestType_Echo;
+            supported = true;
+            break;
+
+          case DIMSE_C_STORE_RQ:
+            request = DicomRequestType_Store;
+            supported = true;
+            break;
+
+          case DIMSE_C_MOVE_RQ:
+            request = DicomRequestType_Move;
+            supported = true;
+            break;
+
+          case DIMSE_C_FIND_RQ:
+            request = DicomRequestType_Find;
+            supported = true;
+            break;
 
-        case DIMSE_C_STORE_RQ:
-          // process C-STORE-Request
-          if (server_.HasStoreRequestHandlerFactory())
-          {
-            std::auto_ptr<IStoreRequestHandler> handler
-              (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
-            cond = Internals::storeScp(assoc_, &msg, presID, *handler);
-          }
-          else
-            cond = DIMSE_BADCOMMANDTYPE;  // Should never happen
-          break;
+          default:
+            // we cannot handle this kind of message
+            cond = DIMSE_BADCOMMANDTYPE;
+            LOG(ERROR) << "cannot handle command: 0x" << std::hex << msg.CommandField;
+            break;
+        }
+
 
-        case DIMSE_C_MOVE_RQ:
-          // process C-MOVE-Request
-          if (server_.HasMoveRequestHandlerFactory())
+        // Check whether this request is allowed by the security filter
+        if (supported && 
+            filter_ != NULL &&
+            !filter_->IsAllowedRequest(callingIP_, callingAETitle_, request))
+        {
+          LOG(ERROR) << EnumerationToString(request) 
+                     << " requests are disallowed for the AET \"" 
+                     << callingAETitle_ << "\"";
+          cond = DIMSE_BADCOMMANDTYPE;
+          supported = false;
+        }
+
+        // in case we received a supported message, process this command
+        if (supported)
+        {
+          // If anything goes wrong, there will be a "BADCOMMANDTYPE" answer
+          cond = DIMSE_BADCOMMANDTYPE;
+
+          switch (request)
           {
-            std::auto_ptr<IMoveRequestHandler> handler
-              (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
-            cond = Internals::moveScp(assoc_, &msg, presID, *handler);
-          }
-          else
-            cond = DIMSE_BADCOMMANDTYPE;  // Should never happen
-          break;
+            case DicomRequestType_Echo:
+              cond = EchoScp(assoc_, &msg, presID);
+              break;
+
+            case DicomRequestType_Store:
+              if (server_.HasStoreRequestHandlerFactory()) // Should always be true
+              {
+                std::auto_ptr<IStoreRequestHandler> handler
+                  (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
+                cond = Internals::storeScp(assoc_, &msg, presID, *handler);
+              }
+              break;
 
-        case DIMSE_C_FIND_RQ:
-          // process C-FIND-Request
-          if (server_.HasFindRequestHandlerFactory())
-          {
-            std::auto_ptr<IFindRequestHandler> handler
-              (server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
-            cond = Internals::findScp(assoc_, &msg, presID, *handler);
+            case DicomRequestType_Move:
+              if (server_.HasMoveRequestHandlerFactory()) // Should always be true
+              {
+                std::auto_ptr<IMoveRequestHandler> handler
+                  (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
+                cond = Internals::moveScp(assoc_, &msg, presID, *handler);
+              }
+              break;
+
+            case DicomRequestType_Find:
+              if (server_.HasFindRequestHandlerFactory()) // Should always be true
+              {
+                std::auto_ptr<IFindRequestHandler> handler
+                  (server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
+                cond = Internals::findScp(assoc_, &msg, presID, *handler);
+              }
+              break;
+
+            default:
+              // Should never happen
+              break;
           }
-          else
-            cond = DIMSE_BADCOMMANDTYPE;  // Should never happen
-          break;
-
-        default:
-          // we cannot handle this kind of message
-          cond = DIMSE_BADCOMMANDTYPE;
-          LOG(ERROR) << "cannot handle command: 0x" << std::hex << msg.CommandField;
-          break;
         }
       }
       else
--- a/OrthancServer/Internals/CommandDispatcher.h	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/Internals/CommandDispatcher.h	Fri Oct 25 12:42:38 2013 +0200
@@ -50,12 +50,21 @@
       uint32_t elapsedTimeSinceLastCommand_;
       const DicomServer& server_;
       T_ASC_Association* assoc_;
+      std::string callingIP_;
+      std::string callingAETitle_;
+      IApplicationEntityFilter* filter_;
 
     public:
       CommandDispatcher(const DicomServer& server,
-                        T_ASC_Association* assoc) : 
+                        T_ASC_Association* assoc,
+                        const std::string& callingIP,
+                        const std::string& callingAETitle,
+                        IApplicationEntityFilter* filter) :
         server_(server),
-        assoc_(assoc)
+        assoc_(assoc),
+        callingIP_(callingIP),
+        callingAETitle_(callingAETitle),
+        filter_(filter)
       {
         clientTimeout_ = server.GetClientTimeout();
         elapsedTimeSinceLastCommand_ = 0;
--- a/OrthancServer/Internals/FindScp.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/Internals/FindScp.cpp	Fri Oct 25 12:42:38 2013 +0200
@@ -74,7 +74,7 @@
 
         try
         {
-          data.handler_->Handle(data.input_, data.answers_);
+          data.handler_->Handle(data.answers_, data.input_);
         }
         catch (OrthancException& e)
         {
--- a/OrthancServer/Internals/MoveScp.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/Internals/MoveScp.cpp	Fri Oct 25 12:42:38 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	Fri Oct 25 12:42:38 2013 +0200
@@ -0,0 +1,440 @@
+/**
+ * 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"
+#include "OrthancInitialization.h"
+
+namespace Orthanc
+{
+  static bool IsWildcard(const std::string& constraint)
+  {
+    return (constraint.find('-') != std::string::npos ||
+            constraint.find('*') != std::string::npos ||
+            constraint.find('\\') != std::string::npos ||
+            constraint.find('?') != std::string::npos);
+  }
+
+  static std::string ToLowerCase(const std::string& s)
+  {
+    std::string result = s;
+    Toolbox::ToLowerCase(result);
+    return result;
+  }
+
+  static bool ApplyRangeConstraint(const std::string& value,
+                                   const std::string& constraint)
+  {
+    size_t separator = constraint.find('-');
+    std::string lower = ToLowerCase(constraint.substr(0, separator));
+    std::string upper = ToLowerCase(constraint.substr(separator + 1));
+    std::string v = ToLowerCase(value);
+
+    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(std::list<std::string>& filteredStudies,
+                                           const std::list<std::string>& studies,
+                                           const DicomMap& input,
+                                           ServerIndex& index)
+  {
+    filteredStudies.clear();
+
+    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 (std::list<std::string>::const_iterator 
+           it = studies.begin(); it != studies.end(); it++)
+    {
+      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, *it, 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.push_back(*it);
+
+                  // 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;
+  }
+
+
+  static bool LookupCandidateResourcesInternal(/* out */ std::list<std::string>& resources,
+                                               /* in */  ServerIndex& index,
+                                               /* in */  ResourceType level,
+                                               /* in */  const DicomMap& query,
+                                               /* in */  DicomTag tag)
+  {
+    if (query.HasTag(tag))
+    {
+      const DicomValue& value = query.GetValue(tag);
+      if (!value.IsNull())
+      {
+        std::string str = query.GetValue(tag).AsString();
+        if (!IsWildcard(str))
+        {
+          index.LookupTagValue(resources, tag, str/*, level*/);
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+
+  static void LookupCandidateResources(/* out */ std::list<std::string>& resources,
+                                       /* in */  ServerIndex& index,
+                                       /* in */  ResourceType level,
+                                       /* in */  const DicomMap& query)
+  {
+    // TODO : Speed up using full querying against the MainDicomTags.
+
+    resources.clear();
+
+    bool done = false;
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_PATIENT_ID);
+        break;
+
+      case ResourceType_Study:
+        done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_STUDY_INSTANCE_UID);
+        break;
+
+      case ResourceType_Series:
+        done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_SERIES_INSTANCE_UID);
+        break;
+
+      case ResourceType_Instance:
+        done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_SOP_INSTANCE_UID);
+        break;
+
+      default:
+        break;
+    }
+
+    if (!done)
+    {
+      Json::Value allResources;
+      index.GetAllUuids(allResources, level);
+      assert(allResources.type() == Json::arrayValue);
+
+      for (Json::Value::ArrayIndex i = 0; i < allResources.size(); i++)
+      {
+        resources.push_back(allResources[i].asString());
+      }
+    }
+  }
+
+
+  void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
+                                         const DicomMap& input)
+  {
+    /**
+     * 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 the candidate resources for this query level. Whenever
+     * possible, we avoid returning ALL the resources for this query
+     * level, as it would imply reading the JSON file on the harddisk
+     * for each of them.
+     **/
+
+    std::list<std::string>  resources;
+    LookupCandidateResources(resources, context_.GetIndex(), level, input);
+
+
+    /**
+     * 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))
+    {
+      std::list<std::string> filtered;
+      if (ApplyModalitiesInStudyFilter(filtered, resources, input, context_.GetIndex()))
+      {
+        resources = filtered;
+      }
+    }
+
+
+    /**
+     * Loop over all the resources for this query level.
+     **/
+
+    DicomArray query(input);
+    for (std::list<std::string>::const_iterator 
+           resource = resources.begin(); resource != resources.end(); resource++)
+    {
+      try
+      {
+        std::string instance;
+        if (LookupOneInstance(instance, context_.GetIndex(), *resource, level))
+        {
+          Json::Value info;
+          context_.ReadJson(info, instance);
+        
+          if (Matches(info, query))
+          {
+            AddAnswer(answers, info, 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	Fri Oct 25 12:42:38 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(DicomFindAnswers& answers,
+                        const DicomMap& input);
+  };
+}
--- a/OrthancServer/OrthancInitialization.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/OrthancInitialization.cpp	Fri Oct 25 12:42:38 2013 +0200
@@ -227,11 +227,11 @@
 
 
 
-  void GetDicomModality(const std::string& name,
-                        std::string& aet,
-                        std::string& address,
-                        int& port,
-                        ModalityManufacturer& manufacturer)
+  void GetDicomModalityUsingSymbolicName(const std::string& name,
+                                         std::string& aet,
+                                         std::string& address,
+                                         int& port,
+                                         ModalityManufacturer& manufacturer)
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
@@ -464,4 +464,87 @@
       target.push_back(lst[i].asString());
     }    
   }
+
+
+  void ConnectToModalityUsingSymbolicName(DicomUserConnection& connection,
+                                          const std::string& name)
+  {
+    std::string aet, address;
+    int port;
+    ModalityManufacturer manufacturer;
+    GetDicomModalityUsingSymbolicName(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();
+  }
+
+
+  bool LookupDicomModalityUsingAETitle(const std::string& aet,
+                                       std::string& symbolicName,
+                                       std::string& address,
+                                       int& port,
+                                       ModalityManufacturer& manufacturer)
+  {
+    std::set<std::string> modalities;
+    GetListOfDicomModalities(modalities);
+
+    for (std::set<std::string>::const_iterator 
+           it = modalities.begin(); it != modalities.end(); it++)
+    {
+      try
+      {
+        std::string thisAet;
+        GetDicomModalityUsingSymbolicName(*it, thisAet, address, port, manufacturer);
+        
+        if (aet == thisAet)
+        {
+          return true;
+        }
+      }
+      catch (OrthancException&)
+      {
+      }
+    }
+
+    return false;
+  }
+
+
+  bool IsKnownAETitle(const std::string& aet)
+  {
+    std::string symbolicName, address;
+    int port;
+    ModalityManufacturer manufacturer;
+    
+    return LookupDicomModalityUsingAETitle(aet, symbolicName, address, port, manufacturer);
+  }
+
+
+  void ConnectToModalityUsingAETitle(DicomUserConnection& connection,
+                                     const std::string& aet)
+  {
+    std::string symbolicName, address;
+    int port;
+    ModalityManufacturer manufacturer;
+
+    if (!LookupDicomModalityUsingAETitle(aet, symbolicName, address, port, manufacturer))
+    {
+      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	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/OrthancInitialization.h	Fri Oct 25 12:42:38 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
@@ -54,11 +55,17 @@
   bool GetGlobalBoolParameter(const std::string& parameter,
                               bool defaultValue);
 
-  void GetDicomModality(const std::string& name,
-                        std::string& aet,
-                        std::string& address,
-                        int& port,
-                        ModalityManufacturer& manufacturer);
+  void GetDicomModalityUsingSymbolicName(const std::string& name,
+                                         std::string& aet,
+                                         std::string& address,
+                                         int& port,
+                                         ModalityManufacturer& manufacturer);
+
+  bool LookupDicomModalityUsingAETitle(const std::string& aet,
+                                       std::string& symbolicName,
+                                       std::string& address,
+                                       int& port,
+                                       ModalityManufacturer& manufacturer);
 
   void GetOrthancPeer(const std::string& name,
                       std::string& url,
@@ -78,4 +85,12 @@
 
   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);
+
+  bool IsKnownAETitle(const std::string& aet);
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Fri Oct 25 12:42:38 2013 +0200
@@ -0,0 +1,179 @@
+/**
+ * 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 "OrthancMoveRequestHandler.h"
+
+#include <glog/logging.h>
+
+#include "DicomProtocol/DicomUserConnection.h"
+#include "OrthancInitialization.h"
+
+namespace Orthanc
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+
+    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;
+      }
+    };
+  }
+
+
+  bool OrthancMoveRequestHandler::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;
+    }
+  }
+
+
+  IMoveRequestIterator* OrthancMoveRequestHandler::Handle(const std::string& target,
+                                                          const DicomMap& input)
+  {
+    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);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancMoveRequestHandler.h	Fri Oct 25 12:42:38 2013 +0200
@@ -0,0 +1,57 @@
+/**
+ * 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/IMoveRequestHandler.h"
+#include "ServerContext.h"
+
+namespace Orthanc
+{
+  class OrthancMoveRequestHandler : public IMoveRequestHandler
+  {
+  private:
+    ServerContext& context_;
+
+    bool LookupResource(std::string& publicId,
+                        DicomTag tag,
+                        const DicomMap& input);
+
+  public:
+    OrthancMoveRequestHandler(ServerContext& context) :
+    context_(context)
+    {
+    }
+
+    virtual IMoveRequestIterator* Handle(const std::string& target,
+                                         const DicomMap& input);
+  };
+}
--- a/OrthancServer/OrthancRestApi.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/OrthancRestApi.cpp	Fri Oct 25 12:42:38 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/ServerEnumerations.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/ServerEnumerations.cpp	Fri Oct 25 12:42:38 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)
   {
@@ -274,6 +254,38 @@
   }
 
 
+  const char* EnumerationToString(DicomRequestType type)
+  {
+    switch (type)
+    {
+      case DicomRequestType_Echo:
+        return "Echo";
+        break;
+
+      case DicomRequestType_Find:
+        return "Find";
+        break;
+
+      case DicomRequestType_Get:
+        return "Get";
+        break;
+
+      case DicomRequestType_Move:
+        return "Move";
+        break;
+
+      case DicomRequestType_Store:
+        return "Store";
+        break;
+
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+
   ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer)
   {
     if (manufacturer == "Generic")
@@ -289,6 +301,4 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
-
-
 }
--- a/OrthancServer/ServerEnumerations.h	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/ServerEnumerations.h	Fri Oct 25 12:42:38 2013 +0200
@@ -33,6 +33,8 @@
 
 #include <string>
 
+#include "../Core/Enumerations.h"
+
 namespace Orthanc
 {
   enum SeriesStatus
@@ -57,6 +59,15 @@
     ModalityManufacturer_ClearCanvas
   };
 
+  enum DicomRequestType
+  {
+    DicomRequestType_Echo,
+    DicomRequestType_Find,
+    DicomRequestType_Get,
+    DicomRequestType_Move,
+    DicomRequestType_Store
+  };
+
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -71,14 +82,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 +125,6 @@
 
   MetadataType StringToMetadata(const std::string& str);
 
-  const char* EnumerationToString(ResourceType type);
-
   std::string EnumerationToString(MetadataType type);
 
   const char* EnumerationToString(SeriesStatus status);
@@ -134,6 +135,8 @@
 
   const char* EnumerationToString(ModalityManufacturer manufacturer);
 
+  const char* EnumerationToString(DicomRequestType type);
+
   ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer);
 
   ResourceType GetParentResourceType(ResourceType type);
--- a/OrthancServer/ServerIndex.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/ServerIndex.cpp	Fri Oct 25 12:42:38 2013 +0200
@@ -1473,6 +1473,29 @@
 
   void ServerIndex::LookupTagValue(std::list<std::string>& result,
                                    DicomTag tag,
+                                   const std::string& value,
+                                   ResourceType type)
+  {
+    result.clear();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    std::list<int64_t> id;
+    db_->LookupTagValue(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));
+      }
+    }
+  }
+
+
+  void ServerIndex::LookupTagValue(std::list<std::string>& result,
+                                   DicomTag tag,
                                    const std::string& value)
   {
     result.clear();
--- a/OrthancServer/ServerIndex.h	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/ServerIndex.h	Fri Oct 25 12:42:38 2013 +0200
@@ -189,6 +189,11 @@
 
     void LookupTagValue(std::list<std::string>& result,
                         DicomTag tag,
+                        const std::string& value,
+                        ResourceType type);
+
+    void LookupTagValue(std::list<std::string>& result,
+                        DicomTag tag,
                         const std::string& value);
 
     void LookupTagValue(std::list<std::string>& result,
--- a/OrthancServer/main.cpp	Fri Oct 25 10:36:06 2013 +0200
+++ b/OrthancServer/main.cpp	Fri Oct 25 12:42:38 2013 +0200
@@ -41,20 +41,23 @@
 #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"
+#include "OrthancMoveRequestHandler.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,47 +75,6 @@
 };
 
 
-class MyFindRequestHandler : public IFindRequestHandler
-{
-private:
-  ServerContext& context_;
-
-public:
-  MyFindRequestHandler(ServerContext& context) :
-    context_(context)
-  {
-  }
-
-  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) :
-    context_(context)
-  {
-  }
-
-public:
-  virtual IMoveRequestIterator* Handle(const std::string& target,
-                                       const DicomMap& input)
-  {
-    LOG(WARNING) << "Move-SCU request received";
-    return NULL;
-  }
-};
-
 
 class MyDicomServerFactory : 
   public IStoreRequestHandlerFactory,
@@ -129,17 +91,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()
@@ -148,6 +110,38 @@
 };
 
 
+class OrthancApplicationEntityFilter : public IApplicationEntityFilter
+{
+public:
+  virtual bool IsAllowedConnection(const std::string& /*callingIp*/,
+                                   const std::string& /*callingAet*/)
+  {
+    return true;
+  }
+
+  virtual bool IsAllowedRequest(const std::string& /*callingIp*/,
+                                const std::string& callingAet,
+                                DicomRequestType type)
+  {
+    if (type == DicomRequestType_Store)
+    {
+      // Incoming store requests are always accepted, even from unknown AET
+      return true;
+    }
+
+    if (!IsKnownAETitle(callingAet))
+    {
+      LOG(ERROR) << "Unkwnown remote DICOM modality AET: \"" << callingAet << "\"";
+      return false;
+    }
+    else
+    {
+      return true;
+    }
+  }
+};
+
+
 class MyIncomingHttpRequestFilter : public IIncomingHttpRequestFilter
 {
 private:
@@ -369,16 +363,17 @@
 
     MyDicomServerFactory serverFactory(context);
     
-
     {
       // DICOM server
       DicomServer dicomServer;
+      OrthancApplicationEntityFilter dicomFilter;
       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"));
+      dicomServer.SetApplicationEntityFilter(dicomFilter);
 
       // HTTP server
       MyIncomingHttpRequestFilter httpFilter(context);
--- a/Resources/CMake/BoostConfiguration.cmake	Fri Oct 25 10:36:06 2013 +0200
+++ b/Resources/CMake/BoostConfiguration.cmake	Fri Oct 25 12:42:38 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	Fri Oct 25 10:36:06 2013 +0200
+++ b/UnitTests/ServerIndex.cpp	Fri Oct 25 12:42:38 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	Fri Oct 25 10:36:06 2013 +0200
+++ b/UnitTests/main.cpp	Fri Oct 25 12:42:38 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.