changeset 1804:bdef349f742c

integration worklists->default
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 20 Nov 2015 16:50:01 +0100
parents 9a3a77d1d2a2 (current diff) d093f998a83b (diff)
children cd213ebcaefd
files
diffstat 67 files changed, 1936 insertions(+), 279 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Nov 19 11:57:32 2015 +0100
+++ b/CMakeLists.txt	Fri Nov 20 16:50:01 2015 +0100
@@ -179,6 +179,8 @@
   OrthancServer/OrthancRestApi/OrthancRestSystem.cpp
   OrthancServer/ParsedDicomFile.cpp
   OrthancServer/QueryRetrieveHandler.cpp
+  OrthancServer/Search/HierarchicalMatcher.cpp
+  OrthancServer/Search/IFindConstraint.cpp
   OrthancServer/Search/LookupIdentifierQuery.cpp
   OrthancServer/Search/LookupResource.cpp
   OrthancServer/Search/SetOfResources.cpp
--- a/Core/Enumerations.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/Core/Enumerations.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -325,6 +325,9 @@
       case ErrorCode_CannotOrderSlices:
         return "Unable to order the slices of the series";
 
+      case ErrorCode_NoWorklistHandler:
+        return "No request handler factory for DICOM C-Find Modality SCP";
+
       default:
         if (error >= ErrorCode_START_PLUGINS)
         {
--- a/Core/Enumerations.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/Core/Enumerations.h	Fri Nov 20 16:50:01 2015 +0100
@@ -138,6 +138,7 @@
     ErrorCode_DatabaseNotInitialized = 2038    /*!< Plugin trying to call the database during its initialization */,
     ErrorCode_SslDisabled = 2039    /*!< Orthanc has been built without SSL support */,
     ErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
+    ErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
     ErrorCode_START_PLUGINS = 1000000
   };
 
--- a/NEWS	Thu Nov 19 11:57:32 2015 +0100
+++ b/NEWS	Fri Nov 20 16:50:01 2015 +0100
@@ -18,6 +18,7 @@
 Plugins
 -------
 
+* New functions "OrthancPluginDicomInstanceToJson()" and "OrthancPluginDicomBufferToJson()"
 * New function "OrthancPluginRegisterErrorCode()" to declare custom error codes
 * New function "OrthancPluginRegisterDictionaryTag()" to declare custom DICOM tags
 * New function "OrthancPluginRestApiGet2()" to provide HTTP headers when calling Orthanc API
--- a/OrthancServer/DicomDirWriter.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomDirWriter.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -527,7 +527,7 @@
       path = directory + '\\' + filename;
     }
 
-    DcmFileFormat& fileFormat = *reinterpret_cast<DcmFileFormat*>(dicom.GetDcmtkObject());
+    DcmFileFormat& fileFormat = dicom.GetDcmtkObject();
 
     DcmDirectoryRecord* instance;
     bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str());
--- a/OrthancServer/DicomInstanceToStore.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomInstanceToStore.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -41,12 +41,6 @@
 
 namespace Orthanc
 {
-  static DcmDataset& GetDataset(ParsedDicomFile& file)
-  {
-    return *reinterpret_cast<DcmFileFormat*>(file.GetDcmtkObject())->getDataset();
-  }
-
-
   void DicomInstanceToStore::AddMetadata(ResourceType level,
                                          MetadataType metadata,
                                          const std::string& value)
@@ -75,7 +69,8 @@
       {
         // Serialize the parsed DICOM file
         buffer_.Allocate();
-        if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), GetDataset(parsed_.GetContent())))
+        if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), 
+                                                 *parsed_.GetContent().GetDcmtkObject().getDataset()))
         {
           LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer";
           throw OrthancException(ErrorCode_InternalError);
@@ -103,14 +98,15 @@
     if (!summary_.HasContent())
     {
       summary_.Allocate();
-      FromDcmtkBridge::Convert(summary_.GetContent(), GetDataset(parsed_.GetContent()));
+      FromDcmtkBridge::Convert(summary_.GetContent(), 
+                               *parsed_.GetContent().GetDcmtkObject().getDataset());
     }
     
     if (!json_.HasContent())
     {
       json_.Allocate();
-      FromDcmtkBridge::ToJson(json_.GetContent(),
-                              GetDataset(parsed_.GetContent()), 
+      FromDcmtkBridge::ToJson(json_.GetContent(), 
+                              *parsed_.GetContent().GetDcmtkObject().getDataset(),
                               DicomToJsonFormat_Full, 
                               DicomToJsonFlags_Default,
                               256 /* max string length */);
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -34,25 +34,165 @@
 #include "DicomFindAnswers.h"
 
 #include "../FromDcmtkBridge.h"
+#include "../ToDcmtkBridge.h"
+#include "../../Core/OrthancException.h"
+
+#include <memory>
+#include <dcmtk/dcmdata/dcfilefo.h>
+
 
 namespace Orthanc
 {
+  class DicomFindAnswers::Answer
+  {
+  private:
+    ParsedDicomFile* dicom_;
+    DicomMap*        map_;
+
+    void CleanupDicom()
+    {
+      if (dicom_ != NULL)
+      {
+        dicom_->Remove(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID);
+        dicom_->Remove(DICOM_TAG_SOP_INSTANCE_UID);
+      }
+    }
+
+  public:
+    Answer(ParsedDicomFile& dicom) : 
+      dicom_(dicom.Clone()),
+      map_(NULL)
+    {
+      CleanupDicom();
+    }
+
+    Answer(const char* dicom,
+           size_t size) : 
+      dicom_(new ParsedDicomFile(dicom, size)),
+      map_(NULL)
+    {
+      CleanupDicom();
+    }
+
+    Answer(const DicomMap& map) : 
+      dicom_(NULL),
+      map_(map.Clone())
+    {
+    }
+
+    ~Answer()
+    {
+      if (dicom_ != NULL)
+      {
+        delete dicom_;
+      }
+
+      if (map_ != NULL)
+      {
+        delete map_;
+      }
+    }
+
+    ParsedDicomFile& GetDicomFile()
+    {
+      if (dicom_ == NULL)
+      {
+        assert(map_ != NULL);
+        dicom_ = new ParsedDicomFile(*map_);
+      }
+
+      return *dicom_;
+    }
+
+    DcmDataset* ExtractDcmDataset() const
+    {
+      if (dicom_ != NULL)
+      {
+        return new DcmDataset(*dicom_->GetDcmtkObject().getDataset());
+      }
+      else
+      {
+        assert(map_ != NULL);
+        return ToDcmtkBridge::Convert(*map_);
+      }
+    }
+  };
+
+
   void DicomFindAnswers::Clear()
   {
-    for (size_t i = 0; i < items_.size(); i++)
+    for (size_t i = 0; i < answers_.size(); i++)
     {
-      delete items_[i];
+      assert(answers_[i] != NULL);
+      delete answers_[i];
+    }
+
+    answers_.clear();
+  }
+
+
+  void DicomFindAnswers::Reserve(size_t size)
+  {
+    if (size > answers_.size())
+    {
+      answers_.reserve(size);
     }
   }
 
-  void DicomFindAnswers::Reserve(size_t size)
+
+  void DicomFindAnswers::Add(const DicomMap& map)
+  {
+    answers_.push_back(new Answer(map));
+  }
+
+
+  void DicomFindAnswers::Add(ParsedDicomFile& dicom)
   {
-    if (size > items_.size())
+    answers_.push_back(new Answer(dicom));
+  }
+
+
+  void DicomFindAnswers::Add(const char* dicom,
+                             size_t size)
+  {
+    answers_.push_back(new Answer(dicom, size));
+  }
+
+
+  DicomFindAnswers::Answer& DicomFindAnswers::GetAnswerInternal(size_t index) const
+  {
+    if (index < answers_.size())
     {
-      items_.reserve(size);
+      return *answers_.at(index);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
 
+
+  ParsedDicomFile& DicomFindAnswers::GetAnswer(size_t index) const
+  {
+    return GetAnswerInternal(index).GetDicomFile();
+  }
+
+
+  DcmDataset* DicomFindAnswers::ExtractDcmDataset(size_t index) const
+  {
+    return GetAnswerInternal(index).ExtractDcmDataset();
+  }
+
+
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                size_t index,
+                                bool simplify) const
+  {
+    DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Simple : DicomToJsonFormat_Full);
+    GetAnswer(index).ToJson(target, format, DicomToJsonFlags_None, 0);
+  }
+
+
   void DicomFindAnswers::ToJson(Json::Value& target,
                                 bool simplify) const
   {
@@ -60,8 +200,8 @@
 
     for (size_t i = 0; i < GetSize(); i++)
     {
-      Json::Value answer(Json::objectValue);
-      FromDcmtkBridge::ToJson(answer, GetAnswer(i), simplify);
+      Json::Value answer;
+      ToJson(answer, i, simplify);
       target.append(answer);
     }
   }
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h	Fri Nov 20 16:50:01 2015 +0100
@@ -32,19 +32,25 @@
 
 #pragma once
 
-#include "../../Core/DicomFormat/DicomMap.h"
-
-#include <vector>
-#include <json/json.h>
+#include "../ParsedDicomFile.h"
 
 namespace Orthanc
 {
-  class DicomFindAnswers
+  class DicomFindAnswers : public boost::noncopyable
   {
   private:
-    std::vector<DicomMap*> items_;
+    class Answer;
+
+    std::vector<Answer*> answers_;
+    bool                 complete_;
+
+    Answer& GetAnswerInternal(size_t index) const;
 
   public:
+    DicomFindAnswers() : complete_(true)
+    {
+    }
+
     ~DicomFindAnswers()
     {
       Clear();
@@ -54,22 +60,37 @@
 
     void Reserve(size_t index);
 
-    void Add(const DicomMap& map)
-    {
-      items_.push_back(map.Clone());
-    }
+    void Add(const DicomMap& map);
+
+    void Add(ParsedDicomFile& dicom);
+
+    void Add(const char* dicom,
+             size_t size);
 
     size_t GetSize() const
     {
-      return items_.size();
+      return answers_.size();
     }
 
-    const DicomMap& GetAnswer(size_t index) const
-    {
-      return *items_.at(index);
-    }
+    ParsedDicomFile& GetAnswer(size_t index) const;
+
+    DcmDataset* ExtractDcmDataset(size_t index) const;
 
     void ToJson(Json::Value& target,
                 bool simplify) const;
+
+    void ToJson(Json::Value& target,
+                size_t index,
+                bool simplify) const;
+
+    bool IsComplete() const
+    {
+      return complete_;
+    }
+
+    void SetComplete(bool isComplete)
+    {
+      complete_ = isComplete;
+    }
   };
 }
--- a/OrthancServer/DicomProtocol/DicomServer.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/DicomServer.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -94,6 +94,7 @@
     findRequestHandlerFactory_ = NULL;
     moveRequestHandlerFactory_ = NULL;
     storeRequestHandlerFactory_ = NULL;
+    worklistRequestHandlerFactory_ = NULL;
     applicationEntityFilter_ = NULL;
     checkCalledAet_ = true;
     clientTimeout_ = 30;
@@ -245,6 +246,29 @@
     }
   }
 
+  void DicomServer::SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& factory)
+  {
+    Stop();
+    worklistRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasWorklistRequestHandlerFactory() const
+  {
+    return (worklistRequestHandlerFactory_ != NULL);
+  }
+
+  IWorklistRequestHandlerFactory& DicomServer::GetWorklistRequestHandlerFactory() const
+  {
+    if (HasWorklistRequestHandlerFactory())
+    {
+      return *worklistRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoWorklistHandler);
+    }
+  }
+
   void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory)
   {
     Stop();
--- a/OrthancServer/DicomProtocol/DicomServer.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/DicomServer.h	Fri Nov 20 16:50:01 2015 +0100
@@ -35,6 +35,7 @@
 #include "IFindRequestHandlerFactory.h"
 #include "IMoveRequestHandlerFactory.h"
 #include "IStoreRequestHandlerFactory.h"
+#include "IWorklistRequestHandlerFactory.h"
 #include "IApplicationEntityFilter.h"
 
 #include <boost/shared_ptr.hpp>
@@ -58,6 +59,7 @@
     IFindRequestHandlerFactory* findRequestHandlerFactory_;
     IMoveRequestHandlerFactory* moveRequestHandlerFactory_;
     IStoreRequestHandlerFactory* storeRequestHandlerFactory_;
+    IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_;
     IApplicationEntityFilter* applicationEntityFilter_;
 
     static void ServerThread(DicomServer* server);
@@ -91,6 +93,10 @@
     bool HasStoreRequestHandlerFactory() const;
     IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const;
 
+    void SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& handler);
+    bool HasWorklistRequestHandlerFactory() const;
+    IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const;
+
     void SetApplicationEntityFilter(IApplicationEntityFilter& handler);
     bool HasApplicationEntityFilter() const;
     IApplicationEntityFilter& GetApplicationEntityFilter() const;
--- a/OrthancServer/DicomProtocol/IApplicationEntityFilter.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/IApplicationEntityFilter.h	Fri Nov 20 16:50:01 2015 +0100
@@ -38,22 +38,25 @@
 
 namespace Orthanc
 {
-  class IApplicationEntityFilter
+  class IApplicationEntityFilter : public boost::noncopyable
   {
   public:
     virtual ~IApplicationEntityFilter()
     {
     }
 
-    virtual bool IsAllowedConnection(const std::string& callingIp,
-                                     const std::string& callingAet) = 0;
+    virtual bool IsAllowedConnection(const std::string& remoteIp,
+                                     const std::string& remoteAet,
+                                     const std::string& calledAet) = 0;
 
-    virtual bool IsAllowedRequest(const std::string& callingIp,
-                                  const std::string& callingAet,
+    virtual bool IsAllowedRequest(const std::string& remoteIp,
+                                  const std::string& remoteAet,
+                                  const std::string& calledAet,
                                   DicomRequestType type) = 0;
 
-    virtual bool IsAllowedTransferSyntax(const std::string& callingIp,
-                                         const std::string& callingAet,
+    virtual bool IsAllowedTransferSyntax(const std::string& remoteIp,
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet,
                                          TransferSyntax syntax) = 0;
   };
 }
--- a/OrthancServer/DicomProtocol/IFindRequestHandler.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/IFindRequestHandler.h	Fri Nov 20 16:50:01 2015 +0100
@@ -34,28 +34,19 @@
 
 #include "DicomFindAnswers.h"
 
-#include <vector>
-#include <string>
-
-
 namespace Orthanc
 {
-  class IFindRequestHandler
+  class IFindRequestHandler : public boost::noncopyable
   {
   public:
     virtual ~IFindRequestHandler()
     {
     }
 
-    /**
-     * Can throw exceptions. Returns "false" iff too many results have
-     * to be returned. In such a case, a "Matching terminated due to
-     * Cancel request" DIMSE code would be returned.
-     * https://www.dabsoft.ch/dicom/4/V.4.1/
-     **/
-    virtual bool Handle(DicomFindAnswers& answers,
+    virtual void Handle(DicomFindAnswers& answers,
                         const DicomMap& input,
                         const std::string& remoteIp,
-                        const std::string& remoteAet) = 0;
+                        const std::string& remoteAet,
+                        const std::string& calledAet) = 0;
   };
 }
--- a/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h	Fri Nov 20 16:50:01 2015 +0100
@@ -36,7 +36,7 @@
 
 namespace Orthanc
 {
-  class IFindRequestHandlerFactory
+  class IFindRequestHandlerFactory : public boost::noncopyable
   {
   public:
     virtual ~IFindRequestHandlerFactory()
--- a/OrthancServer/DicomProtocol/IMoveRequestHandler.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/IMoveRequestHandler.h	Fri Nov 20 16:50:01 2015 +0100
@@ -40,7 +40,7 @@
 
 namespace Orthanc
 {
-  class IMoveRequestIterator
+  class IMoveRequestIterator : public boost::noncopyable
   {
   public:
     enum Status
@@ -70,7 +70,8 @@
     virtual IMoveRequestIterator* Handle(const std::string& target,
                                          const DicomMap& input,
                                          const std::string& remoteIp,
-                                         const std::string& remoteAet) = 0;
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet) = 0;
   };
 
 }
--- a/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h	Fri Nov 20 16:50:01 2015 +0100
@@ -36,7 +36,7 @@
 
 namespace Orthanc
 {
-  class IMoveRequestHandlerFactory
+  class IMoveRequestHandlerFactory : public boost::noncopyable
   {
   public:
     virtual ~IMoveRequestHandlerFactory()
--- a/OrthancServer/DicomProtocol/IStoreRequestHandler.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/IStoreRequestHandler.h	Fri Nov 20 16:50:01 2015 +0100
@@ -40,7 +40,7 @@
 
 namespace Orthanc
 {
-  class IStoreRequestHandler
+  class IStoreRequestHandler : public boost::noncopyable
   {
   public:
     virtual ~IStoreRequestHandler()
--- a/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h	Fri Nov 20 16:50:01 2015 +0100
@@ -36,7 +36,7 @@
 
 namespace Orthanc
 {
-  class IStoreRequestHandlerFactory
+  class IStoreRequestHandlerFactory : public boost::noncopyable
   {
   public:
     virtual ~IStoreRequestHandlerFactory()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomProtocol/IWorklistRequestHandler.h	Fri Nov 20 16:50:01 2015 +0100
@@ -0,0 +1,52 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomFindAnswers.h"
+
+namespace Orthanc
+{
+  class IWorklistRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IWorklistRequestHandler()
+    {
+    }
+
+    virtual void Handle(DicomFindAnswers& answers,
+                        ParsedDicomFile& query,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomProtocol/IWorklistRequestHandlerFactory.h	Fri Nov 20 16:50:01 2015 +0100
@@ -0,0 +1,48 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IWorklistRequestHandler.h"
+
+namespace Orthanc
+{
+  class IWorklistRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IWorklistRequestHandlerFactory()
+    {
+    }
+
+    virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() = 0;
+  };
+}
--- a/OrthancServer/FromDcmtkBridge.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/FromDcmtkBridge.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -1073,6 +1073,9 @@
       case EVR_TM:
         return ValueRepresentation_Time;
 
+      case EVR_SQ:
+        return ValueRepresentation_Sequence;
+
       default:
         return ValueRepresentation_Other;
     }
--- a/OrthancServer/Internals/CommandDispatcher.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Internals/CommandDispatcher.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -430,6 +430,11 @@
         knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
       }
 
+      if (server.HasWorklistRequestHandlerFactory())
+      {
+        knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel);
+      }
+
       // For C-MOVE
       if (server.HasMoveRequestHandlerFactory())
       {
@@ -460,17 +465,17 @@
       }
 
       // Retrieve the AET and the IP address of the remote modality
-      std::string callingAet;
-      std::string callingIp;
+      std::string remoteAet;
+      std::string remoteIp;
       std::string calledAet;
   
       {
-        DIC_AE callingAet_C;
+        DIC_AE remoteAet_C;
         DIC_AE calledAet_C;
-        DIC_AE callingIp_C;
+        DIC_AE remoteIp_C;
         DIC_AE calledIP_C;
-        if (ASC_getAPTitles(assoc->params, callingAet_C, calledAet_C, NULL).bad() ||
-            ASC_getPresentationAddresses(assoc->params, callingIp_C, calledIP_C).bad())
+        if (ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() ||
+            ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad())
         {
           T_ASC_RejectParameters rej =
             {
@@ -483,13 +488,13 @@
           return NULL;
         }
 
-        callingIp = std::string(/*OFSTRING_GUARD*/(callingIp_C));
-        callingAet = std::string(/*OFSTRING_GUARD*/(callingAet_C));
+        remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
+        remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
         calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
       }
 
-      LOG(INFO) << "Association Received from AET " << callingAet 
-                << " on IP " << callingIp;
+      LOG(INFO) << "Association Received from AET " << remoteAet 
+                << " on IP " << remoteIp;
 
 
       std::vector<const char*> transferSyntaxes;
@@ -501,13 +506,13 @@
 
       // New transfer syntaxes supported since Orthanc 0.7.2
       if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Deflated))
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated))
       {
         transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
       }
 
       if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg))
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg))
       {
         transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
         transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
@@ -532,14 +537,14 @@
       }
 
       if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg2000))
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000))
       {
         transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
         transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
       }
 
       if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_JpegLossless))
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless))
       {
         transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
         transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
@@ -548,21 +553,21 @@
       }
 
       if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpip))
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip))
       {
         transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
         transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
       }
 
       if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Mpeg2))
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2))
       {
         transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
         transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
       }
 
       if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Rle))
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle))
       {
         transferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
       }
@@ -638,9 +643,9 @@
       }
 
       if (server.HasApplicationEntityFilter() &&
-          !server.GetApplicationEntityFilter().IsAllowedConnection(callingIp, callingAet))
+          !server.GetApplicationEntityFilter().IsAllowedConnection(remoteIp, remoteAet, calledAet))
       {
-        LOG(WARNING) << "Rejected association for remote AET " << callingAet << " on IP " << callingIp;
+        LOG(WARNING) << "Rejected association for remote AET " << remoteAet << " on IP " << remoteIp;
         T_ASC_RejectParameters rej =
           {
             ASC_RESULT_REJECTEDPERMANENT,
@@ -687,7 +692,7 @@
       }
 
       IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL;
-      return new CommandDispatcher(server, assoc, callingIp, callingAet, filter);
+      return new CommandDispatcher(server, assoc, remoteIp, remoteAet, calledAet, filter);
     }
 
     bool CommandDispatcher::Step()
@@ -771,7 +776,7 @@
         if (supported && 
             request != DicomRequestType_Echo &&  // Always allow incoming ECHO requests
             filter_ != NULL &&
-            !filter_->IsAllowedRequest(remoteIp_, remoteAet_, request))
+            !filter_->IsAllowedRequest(remoteIp_, remoteAet_, calledAet_, request))
         {
           LOG(ERROR) << EnumerationToString(request) 
                      << " requests are disallowed for the AET \"" 
@@ -798,7 +803,11 @@
               {
                 std::auto_ptr<IStoreRequestHandler> handler
                   (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
-                cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_);
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_);
+                }
               }
               break;
 
@@ -807,16 +816,32 @@
               {
                 std::auto_ptr<IMoveRequestHandler> handler
                   (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
-                cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_);
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_);
+                }
               }
               break;
 
             case DicomRequestType_Find:
-              if (server_.HasFindRequestHandlerFactory()) // Should always be true
+              if (server_.HasFindRequestHandlerFactory() || // Should always be true
+                  server_.HasWorklistRequestHandlerFactory())
               {
-                std::auto_ptr<IFindRequestHandler> handler
-                  (server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
-                cond = Internals::findScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_);
+                std::auto_ptr<IFindRequestHandler> findHandler;
+                if (server_.HasFindRequestHandlerFactory())
+                {
+                  findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
+                }
+
+                std::auto_ptr<IWorklistRequestHandler> worklistHandler;
+                if (server_.HasWorklistRequestHandlerFactory())
+                {
+                  worklistHandler.reset(server_.GetWorklistRequestHandlerFactory().ConstructWorklistRequestHandler());
+                }
+
+                cond = Internals::findScp(assoc_, &msg, presID, findHandler.get(), 
+                                          worklistHandler.get(), remoteIp_, remoteAet_, calledAet_);
               }
               break;
 
--- a/OrthancServer/Internals/CommandDispatcher.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Internals/CommandDispatcher.h	Fri Nov 20 16:50:01 2015 +0100
@@ -52,6 +52,7 @@
       T_ASC_Association* assoc_;
       std::string remoteIp_;
       std::string remoteAet_;
+      std::string calledAet_;
       IApplicationEntityFilter* filter_;
 
     public:
@@ -59,11 +60,13 @@
                         T_ASC_Association* assoc,
                         const std::string& remoteIp,
                         const std::string& remoteAet,
+                        const std::string& calledAet,
                         IApplicationEntityFilter* filter) :
         server_(server),
         assoc_(assoc),
         remoteIp_(remoteIp),
         remoteAet_(remoteAet),
+        calledAet_(calledAet),
         filter_(filter)
       {
         clientTimeout_ = server.GetClientTimeout();
--- a/OrthancServer/Internals/FindScp.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Internals/FindScp.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -87,7 +87,7 @@
 #include "../../Core/Logging.h"
 #include "../../Core/OrthancException.h"
 
-
+#include <dcmtk/dcmdata/dcfilefo.h>
 
 namespace Orthanc
 {
@@ -95,13 +95,13 @@
   {  
     struct FindScpData
     {
-      IFindRequestHandler* handler_;
-      DicomMap input_;
+      IFindRequestHandler* findHandler_;
+      IWorklistRequestHandler* worklistHandler_;
       DicomFindAnswers answers_;
       DcmDataset* lastRequest_;
       const std::string* remoteIp_;
       const std::string* remoteAet_;
-      bool noCroppingOfResults_;
+      const std::string* calledAet_;
     };
 
 
@@ -120,20 +120,55 @@
       bzero(response, sizeof(T_DIMSE_C_FindRSP));
       *statusDetail = NULL;
 
+      std::string sopClassUid(request->AffectedSOPClassUID);
+
       FindScpData& data = *reinterpret_cast<FindScpData*>(callbackData);
       if (data.lastRequest_ == NULL)
       {
-        FromDcmtkBridge::Convert(data.input_, *requestIdentifiers);
+        bool ok = false;
 
         try
         {
-          data.noCroppingOfResults_ = data.handler_->Handle(data.answers_, data.input_, 
-                                                            *data.remoteIp_, *data.remoteAet_);
+          if (sopClassUid == UID_FINDModalityWorklistInformationModel)
+          {
+            if (data.worklistHandler_ != NULL)
+            {
+              ParsedDicomFile query(*requestIdentifiers);
+              data.worklistHandler_->Handle(data.answers_, query,
+                                            *data.remoteIp_, *data.remoteAet_,
+                                            *data.calledAet_);
+              ok = true;
+            }
+            else
+            {
+              LOG(ERROR) << "No worklist handler is installed, cannot handle this C-FIND request";
+            }
+          }
+          else
+          {
+            if (data.findHandler_ != NULL)
+            {
+              DicomMap input;
+              FromDcmtkBridge::Convert(input, *requestIdentifiers);
+              data.findHandler_->Handle(data.answers_, input,
+                                        *data.remoteIp_, *data.remoteAet_,
+                                        *data.calledAet_);
+              ok = true;
+            }
+            else
+            {
+              LOG(ERROR) << "No C-Find handler is installed, cannot handle this request";
+            }
+          }
         }
         catch (OrthancException& e)
         {
           // Internal error!
           LOG(ERROR) <<  "C-FIND request handler has failed: " << e.What();
+        }
+
+        if (!ok)
+        {
           response->DimseStatus = STATUS_FIND_Failed_UnableToProcess;
           *responseIdentifiers = NULL;   
           return;
@@ -153,9 +188,9 @@
       {
         // There are pending results that are still to be sent
         response->DimseStatus = STATUS_Pending;
-        *responseIdentifiers = ToDcmtkBridge::Convert(data.answers_.GetAnswer(responseCount - 1));
+        *responseIdentifiers = data.answers_.ExtractDcmDataset(responseCount - 1);
       }
-      else if (data.noCroppingOfResults_)
+      else if (data.answers_.IsComplete())
       {
         // Success: All the results have been sent
         response->DimseStatus = STATUS_Success;
@@ -175,16 +210,19 @@
   OFCondition Internals::findScp(T_ASC_Association * assoc, 
                                  T_DIMSE_Message * msg, 
                                  T_ASC_PresentationContextID presID,
-                                 IFindRequestHandler& handler,
+                                 IFindRequestHandler* findHandler,
+                                 IWorklistRequestHandler* worklistHandler,
                                  const std::string& remoteIp,
-                                 const std::string& remoteAet)
+                                 const std::string& remoteAet,
+                                 const std::string& calledAet)
   {
     FindScpData data;
     data.lastRequest_ = NULL;
-    data.handler_ = &handler;
+    data.findHandler_ = findHandler;
+    data.worklistHandler_ = worklistHandler;
     data.remoteIp_ = &remoteIp;
     data.remoteAet_ = &remoteAet;
-    data.noCroppingOfResults_ = true;
+    data.calledAet_ = &calledAet;
 
     OFCondition cond = DIMSE_findProvider(assoc, presID, &msg->msg.CFindRQ, 
                                           FindScpCallback, &data,
--- a/OrthancServer/Internals/FindScp.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Internals/FindScp.h	Fri Nov 20 16:50:01 2015 +0100
@@ -33,6 +33,7 @@
 #pragma once
 
 #include "../DicomProtocol/IFindRequestHandler.h"
+#include "../DicomProtocol/IWorklistRequestHandler.h"
 
 #include <dcmtk/dcmnet/dimse.h>
 
@@ -43,8 +44,10 @@
     OFCondition findScp(T_ASC_Association * assoc, 
                         T_DIMSE_Message * msg, 
                         T_ASC_PresentationContextID presID,
-                        IFindRequestHandler& handler,
+                        IFindRequestHandler* findHandler,   // can be NULL
+                        IWorklistRequestHandler* worklistHandler,   // can be NULL
                         const std::string& remoteIp,
-                        const std::string& remoteAet);
+                        const std::string& remoteAet,
+                        const std::string& calledAet);
   }
 }
--- a/OrthancServer/Internals/MoveScp.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Internals/MoveScp.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -98,7 +98,6 @@
     {
       std::string target_;
       IMoveRequestHandler* handler_;
-      DicomMap input_;
       DcmDataset* lastRequest_;
       unsigned int subOperationCount_;
       unsigned int failureCount_;
@@ -106,6 +105,7 @@
       std::auto_ptr<IMoveRequestIterator> iterator_;
       const std::string* remoteIp_;
       const std::string* remoteAet_;
+      const std::string* calledAet_;
     };
 
 
@@ -128,12 +128,14 @@
       MoveScpData& data = *reinterpret_cast<MoveScpData*>(callbackData);
       if (data.lastRequest_ == NULL)
       {
-        FromDcmtkBridge::Convert(data.input_, *requestIdentifiers);
+        DicomMap input;
+        FromDcmtkBridge::Convert(input, *requestIdentifiers);
 
         try
         {
-          data.iterator_.reset(data.handler_->Handle(data.target_, data.input_, 
-                                                     *data.remoteIp_, *data.remoteAet_));
+          data.iterator_.reset(data.handler_->Handle(data.target_, input,
+                                                     *data.remoteIp_, *data.remoteAet_,
+                                                     *data.calledAet_));
 
           if (data.iterator_.get() == NULL)
           {
@@ -215,7 +217,8 @@
                                  T_ASC_PresentationContextID presID,
                                  IMoveRequestHandler& handler,
                                  const std::string& remoteIp,
-                                 const std::string& remoteAet)
+                                 const std::string& remoteAet,
+                                 const std::string& calledAet)
   {
     MoveScpData data;
     data.target_ = std::string(msg->msg.CMoveRQ.MoveDestination);
@@ -223,6 +226,7 @@
     data.handler_ = &handler;
     data.remoteIp_ = &remoteIp;
     data.remoteAet_ = &remoteAet;
+    data.calledAet_ = &calledAet;
 
     OFCondition cond = DIMSE_moveProvider(assoc, presID, &msg->msg.CMoveRQ, 
                                           MoveScpCallback, &data,
--- a/OrthancServer/Internals/MoveScp.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Internals/MoveScp.h	Fri Nov 20 16:50:01 2015 +0100
@@ -45,6 +45,7 @@
                         T_ASC_PresentationContextID presID,
                         IMoveRequestHandler& handler,
                         const std::string& remoteIp,
-                        const std::string& remoteAet);
+                        const std::string& remoteAet,
+                        const std::string& calledAet);
   }
 }
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -88,13 +88,14 @@
   }
 
 
-  bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
+  void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
                                          const DicomMap& input,
                                          const std::string& remoteIp,
-                                         const std::string& remoteAet)
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet)
   {
     /**
-     * Ensure that the calling modality is known to Orthanc.
+     * Ensure that the remote modality is known to Orthanc.
      **/
 
     RemoteModalityParameters modality;
@@ -194,7 +195,7 @@
     context_.GetIndex().FindCandidates(resources, instances, finder);
 
     assert(resources.size() == instances.size());
-    bool finished = true;
+    bool complete = true;
 
     for (size_t i = 0; i < instances.size(); i++)
     {
@@ -206,7 +207,7 @@
         if (maxResults != 0 &&
             answers.GetSize() >= maxResults)
         {
-          finished = false;
+          complete = false;
           break;
         }
         else
@@ -218,6 +219,6 @@
 
     LOG(INFO) << "Number of matching resources: " << answers.GetSize();
 
-    return finished;
+    answers.SetComplete(complete);
   }
 }
--- a/OrthancServer/OrthancFindRequestHandler.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.h	Fri Nov 20 16:50:01 2015 +0100
@@ -55,10 +55,11 @@
     {
     }
 
-    virtual bool Handle(DicomFindAnswers& answers,
+    virtual void Handle(DicomFindAnswers& answers,
                         const DicomMap& input,
                         const std::string& remoteIp,
-                        const std::string& remoteAet);
+                        const std::string& remoteAet,
+                        const std::string& calledAet);
 
     unsigned int GetMaxResults() const
     {
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -165,7 +165,8 @@
   IMoveRequestIterator* OrthancMoveRequestHandler::Handle(const std::string& targetAet,
                                                           const DicomMap& input,
                                                           const std::string& remoteIp,
-                                                          const std::string& remoteAet)
+                                                          const std::string& remoteAet,
+                                                          const std::string& calledAet)
   {
     LOG(WARNING) << "Move-SCU request received for AET \"" << targetAet << "\"";
 
--- a/OrthancServer/OrthancMoveRequestHandler.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/OrthancMoveRequestHandler.h	Fri Nov 20 16:50:01 2015 +0100
@@ -54,6 +54,7 @@
     virtual IMoveRequestIterator* Handle(const std::string& targetAet,
                                          const DicomMap& input,
                                          const std::string& remoteIp,
-                                         const std::string& remoteAet);
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet);
   };
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -280,6 +280,18 @@
   }
 
 
+  static void CopyTagIfExists(DicomMap& target,
+                              ParsedDicomFile& source,
+                              const DicomTag& tag)
+  {
+    std::string tmp;
+    if (source.GetTagValue(tmp, tag))
+    {
+      target.SetValue(tag, tmp);
+    }
+  }
+
+
   static void DicomFind(RestApiPostCall& call)
   {
     LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
@@ -303,15 +315,16 @@
     Json::Value result = Json::arrayValue;
     for (size_t i = 0; i < patients.GetSize(); i++)
     {
-      Json::Value patient(Json::objectValue);
-      FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i), true);
+      Json::Value patient;
+      patients.ToJson(patient, i, true);
 
       DicomMap::SetupFindStudyTemplate(m);
       if (!MergeQueryAndTemplate(m, call.GetBodyData(), call.GetBodySize()))
       {
         return;
       }
-      m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
+
+      CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
 
       DicomFindAnswers studies;
       FindStudy(studies, locker.GetConnection(), m);
@@ -321,16 +334,17 @@
       // Loop over the found studies
       for (size_t j = 0; j < studies.GetSize(); j++)
       {
-        Json::Value study(Json::objectValue);
-        FromDcmtkBridge::ToJson(study, studies.GetAnswer(j), true);
+        Json::Value study;
+        studies.ToJson(study, j, true);
 
         DicomMap::SetupFindSeriesTemplate(m);
         if (!MergeQueryAndTemplate(m, call.GetBodyData(), call.GetBodySize()))
         {
           return;
         }
-        m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID);
-        m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID);
+
+        CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_PATIENT_ID);
+        CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID);
 
         DicomFindAnswers series;
         FindSeries(series, locker.GetConnection(), m);
@@ -339,8 +353,8 @@
         study["Series"] = Json::arrayValue;
         for (size_t k = 0; k < series.GetSize(); k++)
         {
-          Json::Value series2(Json::objectValue);
-          FromDcmtkBridge::ToJson(series2, series.GetAnswer(k), true);
+          Json::Value series2;
+          series.ToJson(series2, k, true);
           study["Series"].append(series2);
         }
 
@@ -465,8 +479,13 @@
   static void GetQueryOneAnswer(RestApiGetCall& call)
   {
     size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
+
     QueryAccessor query(call);
-    AnswerDicomMap(call, query->GetAnswer(index), call.HasArgument("simplify"));
+
+    DicomMap map;
+    query->GetAnswer(map, index);
+
+    AnswerDicomMap(call, map, call.HasArgument("simplify"));
   }
 
 
@@ -547,7 +566,9 @@
 
     // Ensure that the answer of interest does exist
     size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
-    query->GetAnswer(index);
+
+    DicomMap map;
+    query->GetAnswer(map, index);
 
     RestApi::AutoListChildren(call);
   }
--- a/OrthancServer/ParsedDicomFile.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/ParsedDicomFile.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -153,7 +153,8 @@
 
 
   // This method can only be called from the constructors!
-  void ParsedDicomFile::Setup(const char* buffer, size_t size)
+  void ParsedDicomFile::Setup(const void* buffer, 
+                              size_t size)
   {
     DcmInputBufferStream is;
     if (size > 0)
@@ -823,7 +824,20 @@
   }
 
 
-  ParsedDicomFile::ParsedDicomFile(const char* content, size_t size) : pimpl_(new PImpl)
+  ParsedDicomFile::ParsedDicomFile(const DicomMap& map) : pimpl_(new PImpl)
+  {
+    std::auto_ptr<DcmDataset> dataset(ToDcmtkBridge::Convert(map));
+
+    // NOTE: This implies an unnecessary memory copy of the dataset, but no way to get around
+    // http://support.dcmtk.org/redmine/issues/544
+    std::auto_ptr<DcmFileFormat> fileFormat(new DcmFileFormat(dataset.get()));
+
+    pimpl_->file_.reset(fileFormat.release());
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(const void* content, 
+                                   size_t size) : pimpl_(new PImpl)
   {
     Setup(content, size);
   }
@@ -851,15 +865,27 @@
   }
 
 
+  ParsedDicomFile::ParsedDicomFile(DcmDataset& dicom) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(new DcmFileFormat(&dicom));
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(DcmFileFormat& dicom) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(new DcmFileFormat(dicom));
+  }
+
+
   ParsedDicomFile::~ParsedDicomFile()
   {
     delete pimpl_;
   }
 
 
-  void* ParsedDicomFile::GetDcmtkObject()
+  DcmFileFormat& ParsedDicomFile::GetDcmtkObject()
   {
-    return pimpl_->file_.get();
+    return *pimpl_->file_.get();
   }
 
 
--- a/OrthancServer/ParsedDicomFile.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/ParsedDicomFile.h	Fri Nov 20 16:50:01 2015 +0100
@@ -39,6 +39,9 @@
 #include "../Core/Images/ImageBuffer.h"
 #include "../Core/IDynamicObject.h"
 
+class DcmDataset;
+class DcmFileFormat;
+
 namespace Orthanc
 {
   class ParsedDicomFile : public IDynamicObject
@@ -49,7 +52,7 @@
 
     ParsedDicomFile(ParsedDicomFile& other);
 
-    void Setup(const char* content,
+    void Setup(const void* content,
                size_t size);
 
     void RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep);
@@ -61,14 +64,20 @@
   public:
     ParsedDicomFile();  // Create a minimal DICOM instance
 
-    ParsedDicomFile(const char* content,
+    ParsedDicomFile(const DicomMap& map);
+
+    ParsedDicomFile(const void* content,
                     size_t size);
 
     ParsedDicomFile(const std::string& content);
 
+    ParsedDicomFile(DcmDataset& dicom);
+
+    ParsedDicomFile(DcmFileFormat& dicom);
+
     ~ParsedDicomFile();
 
-    void* GetDcmtkObject();
+    DcmFileFormat& GetDcmtkObject();
 
     ParsedDicomFile* Clone();
 
--- a/OrthancServer/QueryRetrieveHandler.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/QueryRetrieveHandler.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -95,31 +95,24 @@
   }
 
 
-  const DicomMap& QueryRetrieveHandler::GetAnswer(size_t i)
+  void QueryRetrieveHandler::GetAnswer(DicomMap& target,
+                                       size_t i)
   {
     Run();
-
-    if (i >= answers_.GetSize())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    return answers_.GetAnswer(i);
+    answers_.GetAnswer(i).Convert(target);
   }
 
 
   void QueryRetrieveHandler::Retrieve(const std::string& target,
                                       size_t i)
   {
-    Run();
+    DicomMap map;
+    GetAnswer(map, i);
 
-    if (i >= answers_.GetSize())
     {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
+      locker.GetConnection().Move(target, map);
     }
-
-    ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
-    locker.GetConnection().Move(target, answers_.GetAnswer(i));
   }
 
 
--- a/OrthancServer/QueryRetrieveHandler.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/QueryRetrieveHandler.h	Fri Nov 20 16:50:01 2015 +0100
@@ -85,7 +85,8 @@
 
     size_t GetAnswerCount();
 
-    const DicomMap& GetAnswer(size_t i);
+    void GetAnswer(DicomMap& target,
+                   size_t i);
 
     void Retrieve(const std::string& target,
                   size_t i);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/HierarchicalMatcher.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -0,0 +1,329 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "HierarchicalMatcher.h"
+
+#include "../../Core/OrthancException.h"
+#include "../FromDcmtkBridge.h"
+#include "../ToDcmtkBridge.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+namespace Orthanc
+{
+  HierarchicalMatcher::HierarchicalMatcher(ParsedDicomFile& query,
+                                           bool caseSensitivePN)
+  {
+    Setup(*query.GetDcmtkObject().getDataset(), 
+          caseSensitivePN,
+          query.GetEncoding());
+  }
+
+
+  HierarchicalMatcher::~HierarchicalMatcher()
+  {
+    for (Constraints::iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        delete it->second;
+      }
+    }
+
+    for (Sequences::iterator it = sequences_.begin();
+         it != sequences_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        delete it->second;
+      }
+    }
+  }
+
+
+  void HierarchicalMatcher::Setup(DcmItem& dataset,
+                                  bool caseSensitivePN,
+                                  Encoding encoding)
+  {
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      if (element == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      DicomTag tag(FromDcmtkBridge::Convert(element->getTag()));
+      if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        // Ignore this specific tag
+        continue;
+      }
+
+      ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag);
+
+      if (constraints_.find(tag) != constraints_.end() ||
+          sequences_.find(tag) != sequences_.end())
+      {
+        throw OrthancException(ErrorCode_BadRequest);        
+      }
+
+      if (vr == ValueRepresentation_Sequence)
+      {
+        DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element);
+
+        if (sequence.card() == 0 ||
+            (sequence.card() == 1 && sequence.getItem(0)->card() == 0))
+        {
+          // Universal matching of a sequence
+          sequences_[tag] = NULL;
+        }
+        else if (sequence.card() == 1)
+        {
+          sequences_[tag] = new HierarchicalMatcher(*sequence.getItem(0), caseSensitivePN, encoding);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadRequest);        
+        }
+      }
+      else
+      {
+        std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
+                                        (*element, DicomToJsonFlags_None, encoding));
+
+        if (value->IsBinary() ||
+            value->IsNull())
+        {
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+        else if (value->GetContent().empty())
+        {
+          // This is an universal matcher
+          constraints_[tag] = NULL;
+        }
+        else
+        {
+          // DICOM specifies that searches must be case sensitive, except
+          // for tags with a PN value representation
+          bool sensitive = true;
+          if (vr == ValueRepresentation_PatientName)
+          {
+            sensitive = caseSensitivePN;
+          }
+
+          constraints_[tag] = IFindConstraint::ParseDicomConstraint(tag, value->GetContent(), sensitive);
+        }
+      }
+    }
+  }
+
+
+  std::string HierarchicalMatcher::Format(const std::string& prefix) const
+  {
+    std::string s;
+    
+    for (Constraints::const_iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      s += prefix + it->first.Format() + " ";
+
+      if (it->second == NULL)
+      {
+        s += "*\n";
+      }
+      else
+      {
+        s += it->second->Format() + "\n";
+      }
+    }
+
+    for (Sequences::const_iterator it = sequences_.begin();
+         it != sequences_.end(); ++it)
+    {
+      s += prefix + it->first.Format() + " ";
+
+      if (it->second == NULL)
+      {
+        s += "*\n";
+      }
+      else
+      {
+        s += "Sequence:\n" + it->second->Format(prefix + "  ");
+      }
+    }
+
+    return s;
+  }
+
+
+  bool HierarchicalMatcher::Match(ParsedDicomFile& dicom) const
+  {
+    return MatchInternal(*dicom.GetDcmtkObject().getDataset(),
+                         dicom.GetEncoding());
+  }
+
+
+  bool HierarchicalMatcher::MatchInternal(DcmItem& item,
+                                          Encoding encoding) const
+  {
+    for (Constraints::const_iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
+
+        DcmElement* element = NULL;
+        if (!item.findAndGetElement(tag, element).good() ||
+            element == NULL)
+        {
+          return false;
+        }
+
+        std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
+                                        (*element, DicomToJsonFlags_None, encoding));
+
+        if (value->IsNull() ||
+            value->IsBinary() ||
+            !it->second->Match(value->GetContent()))
+        {
+          return false;
+        }
+      }
+    }
+
+    for (Sequences::const_iterator it = sequences_.begin();
+         it != sequences_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
+
+        DcmSequenceOfItems* sequence = NULL;
+        if (!item.findAndGetSequence(tag, sequence).good() ||
+            sequence == NULL)
+        {
+          return true;
+        }
+
+        bool match = false;
+
+        for (unsigned long i = 0; i < sequence->card(); i++)
+        {
+          if (it->second->MatchInternal(*sequence->getItem(i), encoding))
+          {
+            match = true;
+            break;
+          }
+        }
+
+        if (!match)
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+
+  DcmDataset* HierarchicalMatcher::ExtractInternal(DcmItem& source,
+                                                   Encoding encoding) const
+  {
+    std::auto_ptr<DcmDataset> target(new DcmDataset);
+
+    for (Constraints::const_iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
+      
+      DcmElement* element = NULL;
+      if (source.findAndGetElement(tag, element).good() &&
+          element != NULL)
+      {
+        std::auto_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(it->first));
+        cloned->copyFrom(*element);
+        target->insert(cloned.release());
+      }
+    }
+
+    for (Sequences::const_iterator it = sequences_.begin();
+         it != sequences_.end(); ++it)
+    {
+      DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
+
+      DcmSequenceOfItems* sequence = NULL;
+      if (source.findAndGetSequence(tag, sequence).good() &&
+          sequence != NULL)
+      {
+        std::auto_ptr<DcmSequenceOfItems> cloned(new DcmSequenceOfItems(tag));
+
+        for (unsigned long i = 0; i < sequence->card(); i++)
+        {
+          if (it->second == NULL)
+          {
+            cloned->append(new DcmItem(*sequence->getItem(i)));
+          }
+          else if (it->second->MatchInternal(*sequence->getItem(i), encoding))  // TODO Might be optimized
+          {
+            // It is necessary to encapsulate the child dataset into a
+            // "DcmItem" object before it can be included in a
+            // sequence. Otherwise, "dciodvfy" reports an error "Bad
+            // tag in sequence - Expecting Item or Sequence Delimiter."
+            std::auto_ptr<DcmDataset> child(it->second->ExtractInternal(*sequence->getItem(i), encoding));
+            cloned->append(new DcmItem(*child));
+          }
+        }
+
+        target->insert(cloned.release());
+      }
+    }
+
+    return target.release();
+  }
+
+
+  ParsedDicomFile* HierarchicalMatcher::Extract(ParsedDicomFile& dicom) const
+  {
+    std::auto_ptr<DcmDataset> dataset(ExtractInternal(*dicom.GetDcmtkObject().getDataset(),
+                                                      dicom.GetEncoding()));
+
+    std::auto_ptr<ParsedDicomFile> result(new ParsedDicomFile(*dataset));
+    result->SetEncoding(Encoding_Utf8);
+
+    return result.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/HierarchicalMatcher.h	Fri Nov 20 16:50:01 2015 +0100
@@ -0,0 +1,81 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/DicomFormat/DicomMap.h"
+#include "IFindConstraint.h"
+#include "../ParsedDicomFile.h"
+
+class DcmItem;
+
+namespace Orthanc
+{
+  class HierarchicalMatcher : public boost::noncopyable
+  {
+  private:
+    typedef std::map<DicomTag, IFindConstraint*>      Constraints;
+    typedef std::map<DicomTag, HierarchicalMatcher*>  Sequences;
+
+    Constraints  constraints_;
+    Sequences    sequences_;
+
+    void Setup(DcmItem& query,
+               bool caseSensitivePN,
+               Encoding encoding);
+
+    HierarchicalMatcher(DcmItem& query,
+                        bool caseSensitivePN,
+                        Encoding encoding)
+    {
+      Setup(query, caseSensitivePN, encoding);
+    }
+
+    bool MatchInternal(DcmItem& dicom,
+                       Encoding encoding) const;
+
+    DcmDataset* ExtractInternal(DcmItem& dicom,
+                                Encoding encoding) const;
+
+  public:
+    HierarchicalMatcher(ParsedDicomFile& query,
+                        bool caseSensitivePN);
+
+    ~HierarchicalMatcher();
+
+    std::string Format(const std::string& prefix = "") const;
+
+    bool Match(ParsedDicomFile& dicom) const;
+
+    ParsedDicomFile* Extract(ParsedDicomFile& dicom) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/IFindConstraint.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -0,0 +1,130 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "IFindConstraint.h"
+
+#include "ListConstraint.h"
+#include "RangeConstraint.h"
+#include "ValueConstraint.h"
+#include "WildcardConstraint.h"
+
+#include "../FromDcmtkBridge.h"
+#include "../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  IFindConstraint* IFindConstraint::ParseDicomConstraint(const DicomTag& tag,
+                                                         const std::string& dicomQuery,
+                                                         bool caseSensitive)
+  {
+    ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag);
+
+    if (vr == ValueRepresentation_Sequence)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if ((vr == ValueRepresentation_Date ||
+         vr == ValueRepresentation_DateTime ||
+         vr == ValueRepresentation_Time) &&
+        dicomQuery.find('-') != std::string::npos)
+    {
+      /**
+       * Range matching is only defined for TM, DA and DT value
+       * representations. This code fixes issues 35 and 37.
+       *
+       * Reference: "Range matching is not defined for types of
+       * Attributes other than dates and times", DICOM PS 3.4,
+       * C.2.2.2.5 ("Range Matching").
+       **/
+      size_t separator = dicomQuery.find('-');
+      std::string lower = dicomQuery.substr(0, separator);
+      std::string upper = dicomQuery.substr(separator + 1);
+      return new RangeConstraint(lower, upper, caseSensitive);
+    }
+    else if (dicomQuery.find('\\') != std::string::npos)
+    {
+      std::auto_ptr<ListConstraint> constraint(new ListConstraint(caseSensitive));
+
+      std::vector<std::string> items;
+      Toolbox::TokenizeString(items, dicomQuery, '\\');
+
+      for (size_t i = 0; i < items.size(); i++)
+      {
+        constraint->AddAllowedValue(items[i]);
+      }
+
+      return constraint.release();
+    }
+    else if (dicomQuery.find('*') != std::string::npos ||
+             dicomQuery.find('?') != std::string::npos)
+    {
+      return new WildcardConstraint(dicomQuery, caseSensitive);
+    }
+    else
+    {
+      /**
+       * Case-insensitive match for PN value representation (Patient
+       * Name). Case-senstive match for all the other value
+       * representations.
+       *
+       * Reference: DICOM PS 3.4
+       *   - C.2.2.2.1 ("Single Value Matching") 
+       *   - C.2.2.2.4 ("Wild Card Matching")
+       * http://medical.nema.org/Dicom/2011/11_04pu.pdf
+       *
+       * "Except for Attributes with a PN Value Representation, only
+       * entities with values which match exactly the value specified in the
+       * request shall match. This matching is case-sensitive, i.e.,
+       * sensitive to the exact encoding of the key attribute value in
+       * character sets where a letter may have multiple encodings (e.g.,
+       * based on its case, its position in a word, or whether it is
+       * accented)
+       * 
+       * For Attributes with a PN Value Representation (e.g., Patient Name
+       * (0010,0010)), an application may perform literal matching that is
+       * either case-sensitive, or that is insensitive to some or all
+       * aspects of case, position, accent, or other character encoding
+       * variants."
+       *
+       * (0008,0018) UI SOPInstanceUID     => Case-sensitive
+       * (0008,0050) SH AccessionNumber    => Case-sensitive
+       * (0010,0020) LO PatientID          => Case-sensitive
+       * (0020,000D) UI StudyInstanceUID   => Case-sensitive
+       * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
+       **/
+
+      return new ValueConstraint(dicomQuery, caseSensitive);
+    }
+  }
+}
--- a/OrthancServer/Search/IFindConstraint.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Search/IFindConstraint.h	Fri Nov 20 16:50:01 2015 +0100
@@ -49,5 +49,11 @@
                        const DicomTag& tag) const = 0;
 
     virtual bool Match(const std::string& value) const = 0;
+
+    virtual std::string Format() const = 0;
+
+    static IFindConstraint* ParseDicomConstraint(const DicomTag& tag,
+                                                 const std::string& dicomQuery,
+                                                 bool caseSensitive);
   };
 }
--- a/OrthancServer/Search/ListConstraint.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Search/ListConstraint.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -75,4 +75,23 @@
 
     return allowedValues_.find(v) != allowedValues_.end();
   }
+
+
+  std::string ListConstraint::Format() const
+  {
+    std::string s;
+
+    for (std::set<std::string>::const_iterator
+           it = allowedValues_.begin(); it != allowedValues_.end(); ++it)
+    {
+      if (!s.empty())
+      {
+        s += "\\";
+      }
+
+      s += *it;
+    }
+
+    return s;
+  }
 }
--- a/OrthancServer/Search/ListConstraint.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Search/ListConstraint.h	Fri Nov 20 16:50:01 2015 +0100
@@ -67,5 +67,7 @@
                        const DicomTag& tag) const;
 
     virtual bool Match(const std::string& value) const;
+
+    virtual std::string Format() const;
   };
 }
--- a/OrthancServer/Search/LookupResource.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Search/LookupResource.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -33,11 +33,6 @@
 #include "../PrecompiledHeadersServer.h"
 #include "LookupResource.h"
 
-#include "ListConstraint.h"
-#include "RangeConstraint.h"
-#include "ValueConstraint.h"
-#include "WildcardConstraint.h"
-
 #include "../../Core/OrthancException.h"
 #include "../../Core/FileStorage/StorageAccessor.h"
 #include "../ServerToolbox.h"
@@ -426,85 +421,16 @@
                                           const std::string& dicomQuery,
                                           bool caseSensitive)
   {
-    ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag);
-
     // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
     // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
     if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
     {
       SetModalitiesInStudy(dicomQuery);
     }
-    else if ((vr == ValueRepresentation_Date ||
-              vr == ValueRepresentation_DateTime ||
-              vr == ValueRepresentation_Time) &&
-             dicomQuery.find('-') != std::string::npos)
-    {
-      /**
-       * Range matching is only defined for TM, DA and DT value
-       * representations. This code fixes issues 35 and 37.
-       *
-       * Reference: "Range matching is not defined for types of
-       * Attributes other than dates and times", DICOM PS 3.4,
-       * C.2.2.2.5 ("Range Matching").
-       **/
-      size_t separator = dicomQuery.find('-');
-      std::string lower = dicomQuery.substr(0, separator);
-      std::string upper = dicomQuery.substr(separator + 1);
-      Add(tag, new RangeConstraint(lower, upper, caseSensitive));
-    }
-    else if (dicomQuery.find('\\') != std::string::npos)
-    {
-      std::auto_ptr<ListConstraint> constraint(new ListConstraint(caseSensitive));
-
-      std::vector<std::string> items;
-      Toolbox::TokenizeString(items, dicomQuery, '\\');
-
-      for (size_t i = 0; i < items.size(); i++)
-      {
-        constraint->AddAllowedValue(items[i]);
-      }
-
-      Add(tag, constraint.release());
-    }
-    else if (dicomQuery.find('*') != std::string::npos ||
-             dicomQuery.find('?') != std::string::npos)
+    else 
     {
-      Add(tag, new WildcardConstraint(dicomQuery, caseSensitive));
-    }
-    else
-    {
-      /**
-       * Case-insensitive match for PN value representation (Patient
-       * Name). Case-senstive match for all the other value
-       * representations.
-       *
-       * Reference: DICOM PS 3.4
-       *   - C.2.2.2.1 ("Single Value Matching") 
-       *   - C.2.2.2.4 ("Wild Card Matching")
-       * http://medical.nema.org/Dicom/2011/11_04pu.pdf
-       *
-       * "Except for Attributes with a PN Value Representation, only
-       * entities with values which match exactly the value specified in the
-       * request shall match. This matching is case-sensitive, i.e.,
-       * sensitive to the exact encoding of the key attribute value in
-       * character sets where a letter may have multiple encodings (e.g.,
-       * based on its case, its position in a word, or whether it is
-       * accented)
-       * 
-       * For Attributes with a PN Value Representation (e.g., Patient Name
-       * (0010,0010)), an application may perform literal matching that is
-       * either case-sensitive, or that is insensitive to some or all
-       * aspects of case, position, accent, or other character encoding
-       * variants."
-       *
-       * (0008,0018) UI SOPInstanceUID     => Case-sensitive
-       * (0008,0050) SH AccessionNumber    => Case-sensitive
-       * (0010,0020) LO PatientID          => Case-sensitive
-       * (0020,000D) UI StudyInstanceUID   => Case-sensitive
-       * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
-      **/
-
-      Add(tag, new ValueConstraint(dicomQuery, caseSensitive));
+      Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive));
     }
   }
+
 }
--- a/OrthancServer/Search/RangeConstraint.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Search/RangeConstraint.h	Fri Nov 20 16:50:01 2015 +0100
@@ -64,5 +64,10 @@
                        const DicomTag& tag) const;
 
     virtual bool Match(const std::string& value) const;
+
+    virtual std::string Format() const
+    {
+      return lower_ + "-" + upper_;
+    }
   };
 }
--- a/OrthancServer/Search/ValueConstraint.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Search/ValueConstraint.h	Fri Nov 20 16:50:01 2015 +0100
@@ -61,5 +61,10 @@
                        const DicomTag& tag) const;
 
     virtual bool Match(const std::string& value) const;
+
+    virtual std::string Format() const
+    {
+      return value_;
+    }
   };
 }
--- a/OrthancServer/Search/WildcardConstraint.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Search/WildcardConstraint.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -78,4 +78,9 @@
   {
     lookup.AddConstraint(tag, IdentifierConstraintType_Wildcard, pimpl_->wildcard_);
   }
+
+  std::string WildcardConstraint::Format() const
+  {
+    return pimpl_->wildcard_;
+  }
 }
--- a/OrthancServer/Search/WildcardConstraint.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/Search/WildcardConstraint.h	Fri Nov 20 16:50:01 2015 +0100
@@ -59,5 +59,7 @@
                        const DicomTag& tag) const;
 
     virtual bool Match(const std::string& value) const;
+
+    virtual std::string Format() const;
   };
 }
--- a/OrthancServer/ServerEnumerations.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/ServerEnumerations.h	Fri Nov 20 16:50:01 2015 +0100
@@ -98,7 +98,8 @@
     ValueRepresentation_PatientName,
     ValueRepresentation_Date,
     ValueRepresentation_DateTime,
-    ValueRepresentation_Time
+    ValueRepresentation_Time,
+    ValueRepresentation_Sequence
   };
 
   enum DicomToJsonFormat
--- a/OrthancServer/main.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/OrthancServer/main.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -159,14 +159,16 @@
   {
   }
 
-  virtual bool IsAllowedConnection(const std::string& /*callingIp*/,
-                                   const std::string& /*callingAet*/)
+  virtual bool IsAllowedConnection(const std::string& /*remoteIp*/,
+                                   const std::string& /*remoteAet*/,
+                                   const std::string& /*calledAet*/)
   {
     return true;
   }
 
-  virtual bool IsAllowedRequest(const std::string& /*callingIp*/,
-                                const std::string& callingAet,
+  virtual bool IsAllowedRequest(const std::string& /*remoteIp*/,
+                                const std::string& remoteAet,
+                                const std::string& /*calledAet*/,
                                 DicomRequestType type)
   {
     if (type == DicomRequestType_Store)
@@ -175,9 +177,9 @@
       return true;
     }
 
-    if (!Configuration::IsKnownAETitle(callingAet))
+    if (!Configuration::IsKnownAETitle(remoteAet))
     {
-      LOG(ERROR) << "Unknown remote DICOM modality AET: \"" << callingAet << "\"";
+      LOG(ERROR) << "Unknown remote DICOM modality AET: \"" << remoteAet << "\"";
       return false;
     }
     else
@@ -186,8 +188,9 @@
     }
   }
 
-  virtual bool IsAllowedTransferSyntax(const std::string& callingIp,
-                                       const std::string& callingAet,
+  virtual bool IsAllowedTransferSyntax(const std::string& remoteIp,
+                                       const std::string& remoteAet,
+                                       const std::string& /*calledAet*/,
                                        TransferSyntax syntax)
   {
     std::string configuration;
@@ -234,8 +237,8 @@
       if (locker.GetLua().IsExistingFunction(lua.c_str()))
       {
         LuaFunctionCall call(locker.GetLua(), lua.c_str());
-        call.PushString(callingAet);
-        call.PushString(callingIp);
+        call.PushString(remoteAet);
+        call.PushString(remoteIp);
         return call.ExecutePredicate();
       }
     }
@@ -550,6 +553,7 @@
     PrintErrorCode(ErrorCode_DatabaseNotInitialized, "Plugin trying to call the database during its initialization");
     PrintErrorCode(ErrorCode_SslDisabled, "Orthanc has been built without SSL support");
     PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series");
+    PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP");
   }
 
   std::cout << std::endl;
@@ -704,6 +708,14 @@
   dicomServer.SetStoreRequestHandlerFactory(serverFactory);
   dicomServer.SetMoveRequestHandlerFactory(serverFactory);
   dicomServer.SetFindRequestHandlerFactory(serverFactory);
+
+#if ORTHANC_PLUGINS_ENABLED == 1
+  if (plugins)
+  {
+    dicomServer.SetWorklistRequestHandlerFactory(*plugins);
+  }
+#endif
+
   dicomServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("DicomPort", 4242));
   dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"));
   dicomServer.SetApplicationEntityFilter(dicomFilter);
--- a/Plugins/Engine/OrthancPlugins.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/Plugins/Engine/OrthancPlugins.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -47,6 +47,7 @@
 #include "../../OrthancServer/OrthancInitialization.h"
 #include "../../OrthancServer/ServerContext.h"
 #include "../../OrthancServer/ServerToolbox.h"
+#include "../../OrthancServer/Search/HierarchicalMatcher.h"
 #include "../../Core/Compression/ZlibCompressor.h"
 #include "../../Core/Compression/GzipCompressor.h"
 #include "../../Core/Images/Image.h"
@@ -61,6 +62,46 @@
 
 namespace Orthanc
 {
+  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
+                                 const void* data,
+                                 size_t size)
+  {
+    target.size = size;
+
+    if (size == 0)
+    {
+      target.data = NULL;
+    }
+    else
+    {
+      target.data = malloc(size);
+      if (target.data != NULL)
+      {
+        memcpy(target.data, data, size);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+    }
+  }
+
+
+  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
+                                 const std::string& str)
+  {
+    if (str.size() == 0)
+    {
+      target.size = 0;
+      target.data = NULL;
+    }
+    else
+    {
+      CopyToMemoryBuffer(target, str.c_str(), str.size());
+    }
+  }
+
+
   namespace
   {
     class PluginStorageArea : public IStorageArea
@@ -237,6 +278,7 @@
     typedef std::list<RestCallback*>  RestCallbacks;
     typedef std::list<OrthancPluginOnStoredInstanceCallback>  OnStoredCallbacks;
     typedef std::list<OrthancPluginOnChangeCallback>  OnChangeCallbacks;
+    typedef std::list<OrthancPluginWorklistCallback>  WorklistCallbacks;
     typedef std::map<Property, std::string>  Properties;
 
     PluginsManager manager_;
@@ -244,10 +286,12 @@
     RestCallbacks restCallbacks_;
     OnStoredCallbacks  onStoredCallbacks_;
     OnChangeCallbacks  onChangeCallbacks_;
+    WorklistCallbacks  worklistCallbacks_;
     std::auto_ptr<StorageAreaFactory>  storageArea_;
     boost::recursive_mutex restCallbackMutex_;
     boost::recursive_mutex storedCallbackMutex_;
     boost::recursive_mutex changeCallbackMutex_;
+    boost::recursive_mutex worklistCallbackMutex_;
     boost::recursive_mutex invokeServiceMutex_;
     Properties properties_;
     int argc_;
@@ -265,6 +309,88 @@
 
 
   
+  class OrthancPlugins::WorklistHandler : public IWorklistRequestHandler
+  {
+  private:
+    OrthancPlugins&  that_;
+    std::auto_ptr<HierarchicalMatcher> matcher_;
+    ParsedDicomFile* currentQuery_;
+
+    void Reset()
+    {
+      matcher_.reset(NULL);
+      currentQuery_ = NULL;
+    }
+
+  public:
+    WorklistHandler(OrthancPlugins& that) : that_(that)
+    {
+      Reset();
+    }
+
+    virtual void Handle(DicomFindAnswers& answers,
+                        ParsedDicomFile& query,
+                        const std::string& remoteIp,
+                        const std::string& remoteAet,
+                        const std::string& calledAet)
+    {
+      bool caseSensitivePN = Configuration::GetGlobalBoolParameter("CaseSensitivePN", false);
+      matcher_.reset(new HierarchicalMatcher(query, caseSensitivePN));
+      currentQuery_ = &query;
+
+      {
+        boost::recursive_mutex::scoped_lock lock(that_.pimpl_->worklistCallbackMutex_);
+
+        for (PImpl::WorklistCallbacks::const_iterator
+               callback = that_.pimpl_->worklistCallbacks_.begin(); 
+             callback != that_.pimpl_->worklistCallbacks_.end(); ++callback)
+        {
+          OrthancPluginErrorCode error = (*callback) 
+            (reinterpret_cast<OrthancPluginWorklistAnswers*>(&answers),
+             reinterpret_cast<const OrthancPluginWorklistQuery*>(this),
+             remoteAet.c_str(),
+             calledAet.c_str());
+
+          if (error != OrthancPluginErrorCode_Success)
+          {
+            Reset();
+            that_.GetErrorDictionary().LogError(error, true);
+            throw OrthancException(static_cast<ErrorCode>(error));
+          }
+        }
+      }
+
+      Reset();
+    }
+
+    void GetQueryDicom(OrthancPluginMemoryBuffer& target) const
+    {
+      assert(currentQuery_ != NULL);
+      std::string dicom;
+      currentQuery_->SaveToMemoryBuffer(dicom);
+      CopyToMemoryBuffer(target, dicom.c_str(), dicom.size());
+    }
+
+    bool IsMatch(const void* dicom,
+                 size_t size) const
+    {
+      assert(matcher_.get() != NULL);
+      ParsedDicomFile f(dicom, size);
+      return matcher_->Match(f);
+    }
+
+    void AddAnswer(OrthancPluginWorklistAnswers* answers,
+                   const void* dicom,
+                   size_t size) const
+    {
+      assert(matcher_.get() != NULL);
+      ParsedDicomFile f(dicom, size);
+      std::auto_ptr<ParsedDicomFile> summary(matcher_->Extract(f));
+      reinterpret_cast<DicomFindAnswers*>(answers)->Add(*summary);
+    }
+  };
+
+  
   static char* CopyString(const std::string& str)
   {
     char *result = reinterpret_cast<char*>(malloc(str.size() + 1));
@@ -546,46 +672,6 @@
 
 
 
-  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
-                                 const void* data,
-                                 size_t size)
-  {
-    target.size = size;
-
-    if (size == 0)
-    {
-      target.data = NULL;
-    }
-    else
-    {
-      target.data = malloc(size);
-      if (target.data != NULL)
-      {
-        memcpy(target.data, data, size);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-      }
-    }
-  }
-
-
-  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
-                                 const std::string& str)
-  {
-    if (str.size() == 0)
-    {
-      target.size = 0;
-      target.data = NULL;
-    }
-    else
-    {
-      CopyToMemoryBuffer(target, str.c_str(), str.size());
-    }
-  }
-
-
   void OrthancPlugins::RegisterRestCallback(const void* parameters,
                                             bool lock)
   {
@@ -622,6 +708,17 @@
   }
 
 
+  void OrthancPlugins::RegisterWorklistCallback(const void* parameters)
+  {
+    const _OrthancPluginWorklistCallback& p = 
+      *reinterpret_cast<const _OrthancPluginWorklistCallback*>(parameters);
+
+    LOG(INFO) << "Plugin has registered an modality worklist callback";
+    pimpl_->worklistCallbacks_.push_back(p.callback);
+  }
+
+
+
 
   void OrthancPlugins::AnswerBuffer(const void* parameters)
   {
@@ -1401,6 +1498,10 @@
         RegisterOnChangeCallback(parameters);
         return true;
 
+      case _OrthancPluginService_RegisterWorklistCallback:
+        RegisterWorklistCallback(parameters);
+        return true;
+
       case _OrthancPluginService_AnswerBuffer:
         AnswerBuffer(parameters);
         return true;
@@ -1831,6 +1932,38 @@
         ApplyDicomToJson(service, parameters);
         return true;
 
+      case _OrthancPluginService_AddWorklistAnswer:
+      {
+        const _OrthancPluginWorklistAnswersOperation& p =
+          *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters);
+        reinterpret_cast<const WorklistHandler*>(p.query)->AddAnswer(p.answers, p.dicom, p.size);
+        return true;
+      }
+
+      case _OrthancPluginService_MarkWorklistAnswersIncomplete:
+      {
+        const _OrthancPluginWorklistAnswersOperation& p =
+          *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters);
+        reinterpret_cast<DicomFindAnswers*>(p.answers)->SetComplete(false);
+        return true;
+      }
+
+      case _OrthancPluginService_IsWorklistMatch:
+      {
+        const _OrthancPluginWorklistQueryOperation& p =
+          *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters);
+        *p.isMatch = reinterpret_cast<const WorklistHandler*>(p.query)->IsMatch(p.dicom, p.size);
+        return true;
+      }
+
+      case _OrthancPluginService_GetWorklistQueryDicom:
+      {
+        const _OrthancPluginWorklistQueryOperation& p =
+          *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters);
+        reinterpret_cast<const WorklistHandler*>(p.query)->GetQueryDicom(*p.target);
+        return true;
+      }
+
       default:
       {
         // This service is unknown to the Orthanc plugin engine
@@ -1948,4 +2081,24 @@
   {
     return pimpl_->dictionary_;
   }
+
+
+  IWorklistRequestHandler* OrthancPlugins::ConstructWorklistRequestHandler()
+  {
+    bool hasHandler;
+
+    {
+      boost::recursive_mutex::scoped_lock lock(pimpl_->worklistCallbackMutex_);
+      hasHandler = !pimpl_->worklistCallbacks_.empty();
+    }
+
+    if (hasHandler)
+    {
+      return new WorklistHandler(*this);
+    }
+    else
+    {
+      return NULL;
+    }
+  }
 }
--- a/Plugins/Engine/OrthancPlugins.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/Plugins/Engine/OrthancPlugins.h	Fri Nov 20 16:50:01 2015 +0100
@@ -50,6 +50,7 @@
 #include "../../Core/FileStorage/IStorageArea.h"
 #include "../../Core/HttpServer/IHttpHandler.h"
 #include "../../OrthancServer/IServerListener.h"
+#include "../../OrthancServer/DicomProtocol/IWorklistRequestHandlerFactory.h"
 #include "OrthancPluginDatabase.h"
 #include "PluginsManager.h"
 
@@ -63,12 +64,15 @@
   class OrthancPlugins : 
     public IHttpHandler, 
     public IPluginServiceProvider, 
-    public IServerListener
+    public IServerListener,
+    public IWorklistRequestHandlerFactory
   {
   private:
     struct PImpl;
     boost::shared_ptr<PImpl> pimpl_;
 
+    class WorklistHandler;
+
     void CheckContextAvailable();
 
     void RegisterRestCallback(const void* parameters,
@@ -78,6 +82,8 @@
 
     void RegisterOnChangeCallback(const void* parameters);
 
+    void RegisterWorklistCallback(const void* parameters);
+
     void AnswerBuffer(const void* parameters);
 
     void Redirect(const void* parameters);
@@ -204,6 +210,9 @@
     {
       SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL);
     }
+
+
+    virtual IWorklistRequestHandler* ConstructWorklistRequestHandler();
   };
 }
 
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Nov 19 11:57:32 2015 +0100
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Nov 20 16:50:01 2015 +0100
@@ -18,6 +18,7 @@
  *    - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback().
  *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea().
  *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV2().
+ *    - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback().
  * -# <tt>void OrthancPluginFinalize()</tt>:
  *    This function is invoked by Orthanc during its shutdown. The plugin
  *    must free all its memory.
@@ -49,6 +50,9 @@
  * @defgroup Callbacks Callbacks
  * @brief Functions to register and manage callbacks by the plugins.
  *
+ * @defgroup Worklists Worklists
+ * @brief Functions to register and manage worklists.
+ *
  * @defgroup Orthanc Orthanc
  * @brief Functions to access the content of the Orthanc server.
  **/
@@ -271,6 +275,7 @@
     OrthancPluginErrorCode_DatabaseNotInitialized = 2038    /*!< Plugin trying to call the database during its initialization */,
     OrthancPluginErrorCode_SslDisabled = 2039    /*!< Orthanc has been built without SSL support */,
     OrthancPluginErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
+    OrthancPluginErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
 
     _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
   } OrthancPluginErrorCode;
@@ -398,6 +403,7 @@
     _OrthancPluginService_RegisterStorageArea = 1002,
     _OrthancPluginService_RegisterOnChangeCallback = 1003,
     _OrthancPluginService_RegisterRestCallbackNoLock = 1004,
+    _OrthancPluginService_RegisterWorklistCallback = 1005,
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -462,6 +468,12 @@
     _OrthancPluginService_GetFontInfo = 6010,
     _OrthancPluginService_DrawText = 6011,
 
+    /* Primitives for handling worklists */
+    _OrthancPluginService_AddWorklistAnswer = 7000,
+    _OrthancPluginService_MarkWorklistAnswersIncomplete = 7001,
+    _OrthancPluginService_IsWorklistMatch = 7002,
+    _OrthancPluginService_GetWorklistQueryDicom = 7003,
+
     _OrthancPluginService_INTERNAL = 0x7fffffff
   } _OrthancPluginService;
 
@@ -754,6 +766,22 @@
 
 
   /**
+   * @brief Opaque structure to an object that represents a C-Find query.
+   * @ingroup Worklists
+   **/
+  typedef struct _OrthancPluginWorklistQuery_t OrthancPluginWorklistQuery;
+
+
+
+  /**
+   * @brief Opaque structure to an object that represents the answers to a C-Find query.
+   * @ingroup Worklists
+   **/
+  typedef struct _OrthancPluginWorklistAnswers_t OrthancPluginWorklistAnswers;
+
+
+
+  /**
    * @brief Signature of a callback function that answers to a REST request.
    * @ingroup Callbacks
    **/
@@ -849,6 +877,27 @@
 
 
   /**
+   * @brief Callback to handle the C-Find SCP requests received by Orthanc.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * receives a C-Find SCP request against modality worklists.
+   *
+   * @param answers The target structure where answers must be stored.
+   * @param query The worklist query.
+   * @param remoteAet The Application Entity Title (AET) of the modality from which the request originates.
+   * @param calledAet The Application Entity Title (AET) of the modality that is called by the request.
+   * @return 0 if success, other value if error.
+   * @ingroup Worklists
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWorklistCallback) (
+    OrthancPluginWorklistAnswers*     answers,
+    const OrthancPluginWorklistQuery* query,
+    const char*                       remoteAet,
+    const char*                       calledAet);
+
+
+
+  /**
    * @brief Data structure that contains information about the Orthanc core.
    **/
   typedef struct _OrthancPluginContext_t
@@ -4025,6 +4074,182 @@
     return context->InvokeService(context, _OrthancPluginService_RestApiGet2, &params);
   }
 
+
+
+  typedef struct
+  {
+    OrthancPluginWorklistCallback callback;
+  } _OrthancPluginWorklistCallback;
+
+  /**
+   * @brief Register a callback to handle modality worklists requests.
+   *
+   * This function registers a callback to handle C-Find SCP requests
+   * on modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @ingroup Worklists
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterWorklistCallback(
+    OrthancPluginContext*          context,
+    OrthancPluginWorklistCallback  callback)
+  {
+    _OrthancPluginWorklistCallback params;
+    params.callback = callback;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterWorklistCallback, &params);
+  }
+
+
+  
+  typedef struct
+  {
+    OrthancPluginWorklistAnswers*      answers;
+    const OrthancPluginWorklistQuery*  query;
+    const void*                        dicom;
+    uint32_t                           size;
+  } _OrthancPluginWorklistAnswersOperation;
+
+  /**
+   * @brief Add one answer to some modality worklist request.
+   *
+   * This function adds one worklist (encoded as a DICOM file) to the
+   * set of answers corresponding to some C-Find SCP request against
+   * modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @param query The worklist query, as received by the callback.
+   * @param dicom The worklist to answer, encoded as a DICOM file.
+   * @param size The size of the DICOM file.
+   * @return 0 if success, other value if error.
+   * @ingroup Worklists
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistAddWorklistAnswer(
+    OrthancPluginContext*             context,
+    OrthancPluginWorklistAnswers*     answers,
+    const OrthancPluginWorklistQuery* query,
+    const void*                       dicom,
+    uint32_t                          size)
+  {
+    _OrthancPluginWorklistAnswersOperation params;
+    params.answers = answers;
+    params.query = query;
+    params.dicom = dicom;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_AddWorklistAnswer, &params);
+  }
+
+
+  /**
+   * @brief Mark the set of worklist answers as incomplete.
+   *
+   * This function marks as incomplete the set of answers
+   * corresponding to some C-Find SCP request against modality
+   * worklists. This must be used if canceling the handling of a
+   * request when too many answers are to be returned.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @return 0 if success, other value if error.
+   * @ingroup Worklists
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginMarkWorklistAnswersIncomplete(
+    OrthancPluginContext*          context,
+    OrthancPluginWorklistAnswers*  answers)
+  {
+    _OrthancPluginWorklistAnswersOperation params;
+    params.answers = answers;
+    params.query = NULL;
+    params.dicom = NULL;
+    params.size = 0;
+
+    return context->InvokeService(context, _OrthancPluginService_MarkWorklistAnswersIncomplete, &params);
+  }
+
+
+  typedef struct
+  {
+    const OrthancPluginWorklistQuery*  query;
+    const void*                        dicom;
+    uint32_t                           size;
+    int32_t*                           isMatch;
+    OrthancPluginMemoryBuffer*         target;
+  } _OrthancPluginWorklistQueryOperation;
+
+  /**
+   * @brief Test whether a worklist matches the query.
+   *
+   * This function checks whether one worklist (encoded as a DICOM
+   * file) matches the C-Find SCP query against modality
+   * worklists. This function must be called before adding the
+   * worklist as an answer through
+   * OrthancPluginWorklistAddWorklistAnswer().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The worklist query, as received by the callback.
+   * @param dicom The worklist to answer, encoded as a DICOM file.
+   * @param size The size of the DICOM file.
+   * @return 1 if the worklist matches the query, 0 otherwise.
+   * @ingroup Worklists
+   **/
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginIsWorklistMatch(
+    OrthancPluginContext*              context,
+    const OrthancPluginWorklistQuery*  query,
+    const void*                        dicom,
+    uint32_t                           size)
+  {
+    int32_t isMatch = 0;
+
+    _OrthancPluginWorklistQueryOperation params;
+    params.query = query;
+    params.dicom = dicom;
+    params.size = size;
+    params.isMatch = &isMatch;
+    params.target = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_IsWorklistMatch, &params) == OrthancPluginErrorCode_Success)
+    {
+      return isMatch;
+    }
+    else
+    {
+      /* Error: Assume non-match */
+      return 0;
+    }
+  }
+
+
+  /**
+   * @brief Retrieve the worklist query as a DICOM file.
+   *
+   * This function retrieves the DICOM file that underlies a C-Find
+   * SCP query against modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target Memory buffer where to store the DICOM file. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param query The worklist query, as received by the callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Worklists
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginGetWorklistQueryDicom(
+    OrthancPluginContext*              context,
+    OrthancPluginMemoryBuffer*         target,
+    const OrthancPluginWorklistQuery*  query)
+  {
+    _OrthancPluginWorklistQueryOperation params;
+    params.query = query;
+    params.dicom = NULL;
+    params.size = 0;
+    params.isMatch = NULL;
+    params.target = target;
+
+    return context->InvokeService(context, _OrthancPluginService_GetWorklistQueryDicom, &params);
+  }
+
+
 #ifdef  __cplusplus
 }
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Fri Nov 20 16:50:01 2015 +0100
@@ -0,0 +1,37 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(SampleModalityWorklists)
+
+SET(SAMPLE_MODALITY_WORKLISTS_VERSION "0.0" CACHE STRING "Version of the plugin")
+SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
+SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
+
+SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
+SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost")
+
+set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
+include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
+
+add_library(SampleModalityWorklists SHARED 
+  Plugin.cpp
+  ${JSONCPP_SOURCES}
+  ${BOOST_SOURCES}
+  )
+
+message("Setting the version of the plugin to ${SAMPLE_MODALITY_WORKLISTS_VERSION}")
+add_definitions(
+  -DSAMPLE_MODALITY_WORKLISTS_VERSION="${SAMPLE_MODALITY_WORKLISTS_VERSION}"
+  -DDEFAULT_WORKLISTS_FOLDER="${CMAKE_SOURCE_DIR}/WorklistsDatabase"
+  )
+
+set_target_properties(SampleModalityWorklists PROPERTIES 
+  VERSION ${SAMPLE_MODALITY_WORKLISTS_VERSION} 
+  SOVERSION ${SAMPLE_MODALITY_WORKLISTS_VERSION})
+
+install(
+  TARGETS SampleModalityWorklists
+  RUNTIME DESTINATION lib    # Destination for Windows
+  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -0,0 +1,234 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * 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 <orthanc/OrthancCPlugin.h>
+
+#include <boost/filesystem.hpp>
+#include <json/value.h>
+#include <json/reader.h>
+#include <string.h>
+#include <iostream>
+
+static OrthancPluginContext* context_ = NULL;
+static std::string folder_;
+
+
+static bool ReadFile(std::string& result,
+                     const std::string& path)
+{
+  OrthancPluginMemoryBuffer tmp;
+  if (OrthancPluginReadFile(context_, &tmp, path.c_str()))
+  {
+    return false;
+  }
+  else
+  {
+    result.assign(reinterpret_cast<const char*>(tmp.data), tmp.size);
+    OrthancPluginFreeMemoryBuffer(context_, &tmp);
+    return true;
+  }
+}
+
+
+/**
+ * This is the main function for matching a DICOM worklist against a query.
+ **/
+static OrthancPluginErrorCode  MatchWorklist(OrthancPluginWorklistAnswers*     answers,
+                                             const OrthancPluginWorklistQuery* query,
+                                             const std::string& path)
+{
+  std::string dicom;
+  if (!ReadFile(dicom, path))
+  {
+    // Cannot read this file, ignore this error
+    return OrthancPluginErrorCode_Success;
+  }
+
+  if (OrthancPluginIsWorklistMatch(context_, query, dicom.c_str(), dicom.size()))
+  {
+    // This DICOM file matches the worklist query, add it to the answers
+    return OrthancPluginWorklistAddWorklistAnswer
+      (context_, answers, query, dicom.c_str(), dicom.size());
+  }
+  else
+  {
+    // This DICOM file does not match
+    return OrthancPluginErrorCode_Success;
+  }
+}
+
+
+
+static bool ConvertToJson(Json::Value& result,
+                          char* content)
+{
+  if (content == NULL)
+  {
+    return false;
+  }
+  else
+  {
+    Json::Reader reader;
+    bool success = reader.parse(content, content + strlen(content), result);
+    OrthancPluginFreeString(context_, content);
+    return success;
+  }
+}
+
+
+
+static bool GetQueryDicom(Json::Value& value,
+                          const OrthancPluginWorklistQuery* query)
+{
+  OrthancPluginMemoryBuffer dicom;
+  if (OrthancPluginGetWorklistQueryDicom(context_, &dicom, query))
+  {
+    return false;
+  }
+
+  char* json = OrthancPluginDicomBufferToJson(context_, reinterpret_cast<const char*>(dicom.data),
+                                              dicom.size, 
+                                              OrthancPluginDicomToJsonFormat_Short, 
+                                              static_cast<OrthancPluginDicomToJsonFlags>(0), 0);
+  OrthancPluginFreeMemoryBuffer(context_, &dicom);
+
+  return ConvertToJson(value, json);
+}
+                          
+
+OrthancPluginErrorCode Callback(OrthancPluginWorklistAnswers*     answers,
+                                const OrthancPluginWorklistQuery* query,
+                                const char*                       remoteAet,
+                                const char*                       calledAet)
+{
+  Json::Value json;
+
+  if (!GetQueryDicom(json, query))
+  {
+    return OrthancPluginErrorCode_InternalError;
+  }
+
+  std::cout << "Received worklist query from remote modality " << remoteAet 
+            << ":" << std::endl << json.toStyledString();
+
+  boost::filesystem::path source(folder_);
+  boost::filesystem::directory_iterator end;
+
+  try
+  {
+    for (boost::filesystem::directory_iterator it(source); it != end; ++it)
+    {
+      if (is_regular_file(it->status()))
+      {
+        std::string extension = boost::filesystem::extension(it->path());
+        if (!strcasecmp(".wl", extension.c_str()))
+        {
+          OrthancPluginErrorCode error = MatchWorklist(answers, query, it->path().string());
+          if (error)
+          {
+            OrthancPluginLogError(context_, "Error while adding an answer to a worklist request");
+            return error;
+          }
+        }
+      }
+    }
+  }
+  catch (boost::filesystem::filesystem_error&)
+  {
+    std::string description = std::string("Inexistent folder while scanning for worklists: ") + source.string();
+    OrthancPluginLogError(context_, description.c_str());
+    return OrthancPluginErrorCode_DirectoryExpected;
+  }
+
+  // Uncomment the following line if too many answers are to be returned
+  // OrthancPluginMarkWorklistAnswersIncomplete(context_, answers);
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    context_ = c;
+    OrthancPluginLogWarning(context_, "Storage plugin is initializing");
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      char info[1024];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              context_->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(context_, info);
+      return -1;
+    }
+
+    Json::Value configuration;
+    if (!ConvertToJson(configuration, OrthancPluginGetConfiguration(context_)))
+    {
+      OrthancPluginLogError(context_, "Cannot access the configuration");
+      return -1;
+    }
+
+    if (configuration.isMember("WorklistsFolder"))
+    {
+      if (configuration["WorklistsFolder"].type() != Json::stringValue)
+      {
+        OrthancPluginLogError(context_, "The configuration option \"WorklistsFolder\" must be a string");
+        return -1;
+      }
+
+      folder_ = configuration["WorklistsFolder"].asString();
+    }
+    else
+    {
+      folder_ = DEFAULT_WORKLISTS_FOLDER;
+    }
+
+    std::string message = "The database of worklists will be read from folder: " + folder_;
+    OrthancPluginLogWarning(context_, message.c_str());
+
+    OrthancPluginRegisterWorklistCallback(context_, Callback);
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPluginLogWarning(context_, "Sample worklist plugin is finalizing");
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "sample-worklists";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return SAMPLE_MODALITY_WORKLISTS_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ModalityWorklists/WorklistsDatabase/Generate.py	Fri Nov 20 16:50:01 2015 +0100
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+
+import os
+import subprocess
+
+SOURCE = '/home/jodogne/Downloads/dcmtk-3.6.0/dcmwlm/data/wlistdb/OFFIS/'
+TARGET = os.path.abspath(os.path.dirname(__file__))
+
+for f in os.listdir(SOURCE):
+    ext = os.path.splitext(f)
+
+    if ext[1].lower() == '.dump':
+        subprocess.check_call([
+            'dump2dcm',
+            '-g',
+            '-q',
+            os.path.join(SOURCE, f),
+            os.path.join(TARGET, ext[0].lower() + '.wl'),
+        ])
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist1.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist10.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist2.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist3.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist4.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist5.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist6.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist7.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist8.wl has changed
Binary file Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist9.wl has changed
--- a/Resources/DicomConformanceStatement.txt	Thu Nov 19 11:57:32 2015 +0100
+++ b/Resources/DicomConformanceStatement.txt	Fri Nov 20 16:50:01 2015 +0100
@@ -149,6 +149,7 @@
 
   FINDPatientRootQueryRetrieveInformationModel   | 1.2.840.10008.5.1.4.1.2.1.1
   FINDStudyRootQueryRetrieveInformationModel     | 1.2.840.10008.5.1.4.1.2.2.1
+  FINDModalityWorklistInformationModel           | 1.2.840.10008.5.1.4.31
 
 
 --------------------
--- a/Resources/ErrorCodes.json	Thu Nov 19 11:57:32 2015 +0100
+++ b/Resources/ErrorCodes.json	Fri Nov 20 16:50:01 2015 +0100
@@ -513,5 +513,10 @@
     "Code": 2040,
     "Name": "CannotOrderSlices",
     "Description": "Unable to order the slices of the series"
+  },
+  {
+    "Code": 2041, 
+    "Name": "NoWorklistHandler", 
+    "Description": "No request handler factory for DICOM C-Find Modality SCP"
   }
 ]
--- a/UnitTestsSources/FromDcmtkTests.cpp	Thu Nov 19 11:57:32 2015 +0100
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Fri Nov 20 16:50:01 2015 +0100
@@ -43,6 +43,7 @@
 #include "../Core/Images/PngWriter.h"
 #include "../Core/Uuid.h"
 #include "../Resources/EncodingTests.h"
+#include "../OrthancServer/DicomProtocol/DicomFindAnswers.h"
 
 #include <dcmtk/dcmdata/dcelem.h>
 
@@ -606,3 +607,33 @@
   ASSERT_EQ("application/octet-stream", mime);
   ASSERT_EQ("Pixels", content);
 }
+
+
+TEST(DicomFindAnswers, Basic)
+{
+  DicomFindAnswers a;
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_PATIENT_ID, "hello");
+    a.Add(m);
+  }
+
+  {
+    ParsedDicomFile d;
+    d.Replace(DICOM_TAG_PATIENT_ID, "my");
+    a.Add(d);
+  }
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_PATIENT_ID, "world");
+    a.Add(m);
+  }
+
+  Json::Value j;
+  a.ToJson(j, true);
+  ASSERT_EQ(3u, j.size());
+
+  //std::cout << j;
+}