changeset 4240:799c0c527ced

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 09 Oct 2020 12:02:40 +0200
parents c8754c4c1862
children 3510da0e260c
files OrthancServer/CMakeLists.txt OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/OrthancWebDav.h OrthancServer/Sources/main.cpp
diffstat 4 files changed, 1578 insertions(+), 1605 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancServer/CMakeLists.txt	Fri Oct 09 11:38:03 2020 +0200
+++ b/OrthancServer/CMakeLists.txt	Fri Oct 09 12:02:40 2020 +0200
@@ -116,6 +116,7 @@
   ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestModalities.cpp
   ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestResources.cpp
   ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestSystem.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp
   ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraint.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Fri Oct 09 12:02:40 2020 +0200
@@ -0,0 +1,1446 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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 "OrthancWebDav.h"
+
+#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
+#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h"
+#include "Search/DatabaseLookup.h"
+#include "ServerContext.h"
+
+#include <boost/regex.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+
+static const char* const BY_PATIENTS = "by-patients";
+static const char* const BY_STUDIES = "by-studies";
+static const char* const BY_DATE = "by-dates";
+static const char* const BY_UIDS = "by-uids";
+static const char* const UPLOADS = "uploads";
+static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
+
+
+namespace Orthanc
+{
+  static boost::posix_time::ptime GetNow()
+  {
+    return boost::posix_time::second_clock::universal_time();
+  }
+  
+
+  static void LookupTime(boost::posix_time::ptime& target,
+                         ServerContext& context,
+                         const std::string& publicId,
+                         MetadataType metadata)
+  {
+    std::string value;
+    if (context.GetIndex().LookupMetadata(value, publicId, metadata))
+    {
+      try
+      {
+        target = boost::posix_time::from_iso_string(value);
+        return;
+      }
+      catch (std::exception& e)
+      {
+      }
+    }
+
+    target = GetNow();
+  }
+
+  
+  class OrthancWebDav::DicomIdentifiersVisitor : public ServerContext::ILookupVisitor
+  {
+  private:
+    ServerContext&  context_;
+    bool            isComplete_;
+    Collection&     target_;
+    ResourceType    level_;
+
+  public:
+    DicomIdentifiersVisitor(ServerContext& context,
+                            Collection&  target,
+                            ResourceType level) :
+      context_(context),
+      isComplete_(false),
+      target_(target),
+      level_(level)
+    {
+    }
+      
+    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
+    {
+      return false;   // (*)
+    }
+      
+    virtual void MarkAsComplete() ORTHANC_OVERRIDE
+    {
+      isComplete_ = true;  // TODO
+    }
+
+    virtual void Visit(const std::string& publicId,
+                       const std::string& instanceId   /* unused     */,
+                       const DicomMap& mainDicomTags,
+                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+    {
+      DicomTag tag(0, 0);
+      MetadataType timeMetadata;
+
+      switch (level_)
+      {
+        case ResourceType_Study:
+          tag = DICOM_TAG_STUDY_INSTANCE_UID;
+          timeMetadata = MetadataType_LastUpdate;
+          break;
+
+        case ResourceType_Series:
+          tag = DICOM_TAG_SERIES_INSTANCE_UID;
+          timeMetadata = MetadataType_LastUpdate;
+          break;
+        
+        case ResourceType_Instance:
+          tag = DICOM_TAG_SOP_INSTANCE_UID;
+          timeMetadata = MetadataType_Instance_ReceptionDate;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+        
+      std::string s;
+      if (mainDicomTags.LookupStringValue(s, tag, false) &&
+          !s.empty())
+      {
+        std::unique_ptr<Resource> resource;
+
+        if (level_ == ResourceType_Instance)
+        {
+          FileInfo info;
+          if (context_.GetIndex().LookupAttachment(info, publicId, FileContentType_Dicom))
+          {
+            std::unique_ptr<File> f(new File(s + ".dcm"));
+            f->SetMimeType(MimeType_Dicom);
+            f->SetContentLength(info.GetUncompressedSize());
+            resource.reset(f.release());
+          }
+        }
+        else
+        {
+          resource.reset(new Folder(s));
+        }
+
+        if (resource.get() != NULL)
+        {
+          boost::posix_time::ptime t;
+          LookupTime(t, context_, publicId, timeMetadata);
+          resource->SetCreationTime(t);
+          target_.AddResource(resource.release());
+        }
+      }
+    }
+  };
+
+  
+  class OrthancWebDav::DicomFileVisitor : public ServerContext::ILookupVisitor
+  {
+  private:
+    ServerContext&  context_;
+    bool            success_;
+    std::string&    target_;
+    boost::posix_time::ptime&  time_;
+
+  public:
+    DicomFileVisitor(ServerContext& context,
+                     std::string& target,
+                     boost::posix_time::ptime& time) :
+      context_(context),
+      success_(false),
+      target_(target),
+      time_(time)
+    {
+    }
+
+    bool IsSuccess() const
+    {
+      return success_;
+    }
+
+    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
+    {
+      return false;   // (*)
+    }
+      
+    virtual void MarkAsComplete() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void Visit(const std::string& publicId,
+                       const std::string& instanceId   /* unused     */,
+                       const DicomMap& mainDicomTags,
+                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+    {
+      if (success_)
+      {
+        success_ = false;  // Two matches => Error
+      }
+      else
+      {
+        LookupTime(time_, context_, publicId, MetadataType_Instance_ReceptionDate);
+        context_.ReadDicom(target_, publicId);
+        success_ = true;
+      }
+    }
+  };
+  
+
+  class OrthancWebDav::OrthancJsonVisitor : public ServerContext::ILookupVisitor
+  {
+  private:
+    ServerContext&  context_;
+    bool            success_;
+    std::string&    target_;
+    ResourceType    level_;
+
+  public:
+    OrthancJsonVisitor(ServerContext& context,
+                       std::string& target,
+                       ResourceType level) :
+      context_(context),
+      success_(false),
+      target_(target),
+      level_(level)
+    {
+    }
+
+    bool IsSuccess() const
+    {
+      return success_;
+    }
+
+    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
+    {
+      return false;   // (*)
+    }
+      
+    virtual void MarkAsComplete() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void Visit(const std::string& publicId,
+                       const std::string& instanceId   /* unused     */,
+                       const DicomMap& mainDicomTags,
+                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+    {
+      Json::Value info;
+      if (context_.GetIndex().LookupResource(info, publicId, level_))
+      {
+        if (success_)
+        {
+          success_ = false;  // Two matches => Error
+        }
+        else
+        {
+          target_ = info.toStyledString();
+
+          // Replace UNIX newlines with DOS newlines 
+          boost::replace_all(target_, "\n", "\r\n");
+
+          success_ = true;
+        }
+      }
+    }
+  };
+
+
+  class OrthancWebDav::ResourcesIndex : public boost::noncopyable
+  {
+  public:
+    typedef std::map<std::string, std::string>   Map;
+
+  private:
+    ServerContext&  context_;
+    ResourceType    level_;
+    std::string     template_;
+    Map             pathToResource_;
+    Map             resourceToPath_;
+
+    void CheckInvariants()
+    {
+#ifndef NDEBUG
+      assert(pathToResource_.size() == resourceToPath_.size());
+
+      for (Map::const_iterator it = pathToResource_.begin(); it != pathToResource_.end(); ++it)
+      {
+        assert(resourceToPath_[it->second] == it->first);
+      }
+
+      for (Map::const_iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it)
+      {
+        assert(pathToResource_[it->second] == it->first);
+      }
+#endif
+    }      
+
+    void AddTags(DicomMap& target,
+                 const std::string& resourceId,
+                 ResourceType tagsFromLevel)
+    {
+      DicomMap tags;
+      if (context_.GetIndex().GetMainDicomTags(tags, resourceId, level_, tagsFromLevel))
+      {
+        target.Merge(tags);
+      }
+    }
+
+    void Register(const std::string& resourceId)
+    {
+      // Don't register twice the same resource
+      if (resourceToPath_.find(resourceId) == resourceToPath_.end())
+      {
+        std::string name = template_;
+
+        DicomMap tags;
+
+        AddTags(tags, resourceId, level_);
+        
+        if (level_ == ResourceType_Study)
+        {
+          AddTags(tags, resourceId, ResourceType_Patient);
+        }
+        
+        DicomArray arr(tags);
+        for (size_t i = 0; i < arr.GetSize(); i++)
+        {
+          const DicomElement& element = arr.GetElement(i);
+          if (!element.GetValue().IsNull() &&
+              !element.GetValue().IsBinary())
+          {
+            const std::string tag = FromDcmtkBridge::GetTagName(element.GetTag(), "");
+            boost::replace_all(name, "{{" + tag + "}}", element.GetValue().GetContent());
+          } 
+        }
+
+        // Blank the tags that were not matched
+        static const boost::regex REGEX_BLANK_TAGS("{{.*?}}");  // non-greedy match
+        name = boost::regex_replace(name, REGEX_BLANK_TAGS, "");
+
+        // UTF-8 characters cannot be used on Windows XP
+        name = Toolbox::ConvertToAscii(name);
+        boost::replace_all(name, "/", "");
+        boost::replace_all(name, "\\", "");
+
+        // Trim sequences of spaces as one single space
+        static const boost::regex REGEX_TRIM_SPACES("{{.*?}}");
+        name = boost::regex_replace(name, REGEX_TRIM_SPACES, " ");
+        name = Toolbox::StripSpaces(name);
+
+        size_t count = 0;
+        for (;;)
+        {
+          std::string path = name;
+          if (count > 0)
+          {
+            path += " (" + boost::lexical_cast<std::string>(count) + ")";
+          }
+
+          if (pathToResource_.find(path) == pathToResource_.end())
+          {
+            pathToResource_[path] = resourceId;
+            resourceToPath_[resourceId] = path;
+            return;
+          }
+
+          count++;
+        }
+
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+  public:
+    ResourcesIndex(ServerContext& context,
+                   ResourceType level,
+                   const std::string& templateString) :
+      context_(context),
+      level_(level),
+      template_(templateString)
+    {
+    }
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void Refresh(std::set<std::string>& removedPaths /* out */,
+                 const std::set<std::string>& resources)
+    {
+      CheckInvariants();
+
+      // Detect the resources that have been removed since last refresh
+      removedPaths.clear();
+      std::set<std::string> removedResources;
+      
+      for (Map::iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it)
+      {
+        if (resources.find(it->first) == resources.end())
+        {
+          const std::string& path = it->second;
+          
+          assert(pathToResource_.find(path) != pathToResource_.end());
+          pathToResource_.erase(path);
+          removedPaths.insert(path);
+          
+          removedResources.insert(it->first);  // Delay the removal to avoid disturbing the iterator
+        }
+      }
+
+      // Remove the missing resources
+      for (std::set<std::string>::const_iterator it = removedResources.begin(); it != removedResources.end(); ++it)
+      {
+        assert(resourceToPath_.find(*it) != resourceToPath_.end());
+        resourceToPath_.erase(*it);
+      }
+
+      CheckInvariants();
+
+      for (std::set<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it)
+      {
+        Register(*it);
+      }
+
+      CheckInvariants();
+    }
+
+    const Map& GetPathToResource() const
+    {
+      return pathToResource_;
+    }
+  };
+
+
+  class OrthancWebDav::InstancesOfSeries : public INode
+  {
+  private:
+    ServerContext&  context_;
+    std::string     parentSeries_;
+
+  public:
+    InstancesOfSeries(ServerContext& context,
+                      const std::string& parentSeries) :
+      context_(context),
+      parentSeries_(parentSeries)
+    {
+    }
+
+    virtual bool ListCollection(IWebDavBucket::Collection& target,
+                                const UriComponents& path) ORTHANC_OVERRIDE
+    {
+      if (path.empty())
+      {
+        std::list<std::string> resources;
+        try
+        {
+          context_.GetIndex().GetChildren(resources, parentSeries_);
+        }
+        catch (OrthancException&)
+        {
+          // Unknown (or deleted) parent series
+          return false;
+        }
+
+        for (std::list<std::string>::const_iterator
+               it = resources.begin(); it != resources.end(); ++it)
+        {
+          boost::posix_time::ptime time;
+          LookupTime(time, context_, *it, MetadataType_Instance_ReceptionDate);
+
+          FileInfo info;
+          if (context_.GetIndex().LookupAttachment(info, *it, FileContentType_Dicom))
+          {
+            std::unique_ptr<File> resource(new File(*it + ".dcm"));
+            resource->SetMimeType(MimeType_Dicom);
+            resource->SetContentLength(info.GetUncompressedSize());
+            resource->SetCreationTime(time);
+            target.AddResource(resource.release());
+          }          
+        }
+        
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    virtual bool GetFileContent(MimeType& mime,
+                                std::string& content,
+                                boost::posix_time::ptime& time, 
+                                const UriComponents& path) ORTHANC_OVERRIDE
+    {
+      if (path.size() == 1 &&
+          boost::ends_with(path[0], ".dcm"))
+      {
+        std::string instanceId = path[0].substr(0, path[0].size() - 4);
+
+        try
+        {
+          mime = MimeType_Dicom;
+          context_.ReadDicom(content, instanceId);
+          LookupTime(time, context_, instanceId, MetadataType_Instance_ReceptionDate);
+          return true;
+        }
+        catch (OrthancException&)
+        {
+          // File was removed
+          return false;
+        }
+      }
+      else
+      {
+        return false;
+      }
+    }
+  };
+
+
+
+  /**
+   * The "InternalNode" class corresponds to a non-leaf node in the
+   * WebDAV tree, that only contains subfolders (no file).
+   * 
+   * TODO: Implement a LRU index to dynamically remove the oldest
+   * children on high RAM usage.
+   **/
+  class OrthancWebDav::InternalNode : public INode
+  {
+  private:
+    typedef std::map<std::string, INode*>  Children;
+
+    Children  children_;
+
+    INode* GetChild(const std::string& path)  // Don't delete the result pointer!
+    {
+      Children::const_iterator child = children_.find(path);
+      if (child == children_.end())
+      {
+        INode* child = CreateChild(path);
+        
+        if (child == NULL)
+        {
+          return NULL;
+        }
+        else
+        {
+          children_[path] = child;
+          return child;
+        }
+      }
+      else
+      {
+        assert(child->second != NULL);
+        return child->second;
+      }
+    }
+
+  protected:
+    void RemoveSubfolder(const std::string& path)
+    {
+      Children::iterator child = children_.find(path);
+      if (child != children_.end())
+      {
+        assert(child->second != NULL);
+        delete child->second;
+        children_.erase(child);
+      }
+    }
+    
+    virtual void Refresh() = 0;
+    
+    virtual bool ListSubfolders(IWebDavBucket::Collection& target) = 0;
+
+    virtual INode* CreateChild(const std::string& path) = 0;
+
+  public:
+    virtual ~InternalNode()
+    {
+      for (Children::iterator it = children_.begin(); it != children_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        delete it->second;
+      }
+    }
+
+    virtual bool ListCollection(IWebDavBucket::Collection& target,
+                                const UriComponents& path)
+      ORTHANC_OVERRIDE ORTHANC_FINAL
+    {
+      Refresh();
+      
+      if (path.empty())
+      {
+        return ListSubfolders(target);
+      }
+      else
+      {
+        // Recursivity
+        INode* child = GetChild(path[0]);
+        if (child == NULL)
+        {
+          return false;
+        }
+        else
+        {
+          UriComponents subpath(path.begin() + 1, path.end());
+          return child->ListCollection(target, subpath);
+        }
+      }
+    }
+
+    virtual bool GetFileContent(MimeType& mime,
+                                std::string& content,
+                                boost::posix_time::ptime& time, 
+                                const UriComponents& path)
+      ORTHANC_OVERRIDE ORTHANC_FINAL
+    {
+      if (path.empty())
+      {
+        return false;  // An internal node doesn't correspond to a file
+      }
+      else
+      {
+        // Recursivity
+        Refresh();
+      
+        INode* child = GetChild(path[0]);
+        if (child == NULL)
+        {
+          return false;
+        }
+        else
+        {
+          UriComponents subpath(path.begin() + 1, path.end());
+          return child->GetFileContent(mime, content, time, subpath);
+        }
+      }
+    }
+  };
+  
+
+  class OrthancWebDav::ListOfResources : public InternalNode
+  {
+  private:
+    ServerContext&                   context_;
+    const Templates&                 templates_;
+    std::unique_ptr<ResourcesIndex>  index_;
+    MetadataType                     timeMetadata_;
+
+  protected:
+    virtual void Refresh() ORTHANC_OVERRIDE ORTHANC_FINAL
+    {
+      std::list<std::string> resources;
+      GetCurrentResources(resources);
+
+      std::set<std::string> removedPaths;
+      index_->Refresh(removedPaths, std::set<std::string>(resources.begin(), resources.end()));
+
+      // Remove the children whose associated resource doesn't exist anymore
+      for (std::set<std::string>::const_iterator
+             it = removedPaths.begin(); it != removedPaths.end(); ++it)
+      {
+        RemoveSubfolder(*it);
+      }
+    }
+
+    virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE ORTHANC_FINAL
+    {
+      if (index_->GetLevel() == ResourceType_Instance)
+      {
+        // Not a collection, no subfolders
+        return false;
+      }
+      else
+      {
+        const ResourcesIndex::Map& paths = index_->GetPathToResource();
+        
+        for (ResourcesIndex::Map::const_iterator it = paths.begin(); it != paths.end(); ++it)
+        {
+          boost::posix_time::ptime time;
+          LookupTime(time, context_, it->second, timeMetadata_);
+
+          std::unique_ptr<IWebDavBucket::Resource> resource(new IWebDavBucket::Folder(it->first));
+          resource->SetCreationTime(time);
+          target.AddResource(resource.release());
+        }
+
+        return true;
+      }
+    }
+
+    virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE ORTHANC_FINAL
+    {
+      ResourcesIndex::Map::const_iterator resource = index_->GetPathToResource().find(path);
+      if (resource == index_->GetPathToResource().end())
+      {
+        return NULL;
+      }
+      else
+      {
+        return CreateResourceNode(resource->second);
+      }
+    }
+
+    ServerContext& GetContext() const
+    {
+      return context_;
+    }
+    
+    virtual void GetCurrentResources(std::list<std::string>& resources) = 0;
+
+    virtual INode* CreateResourceNode(const std::string& resource) = 0;
+    
+  public:
+    ListOfResources(ServerContext& context,
+                    ResourceType level,
+                    const Templates& templates) :
+      context_(context),
+      templates_(templates)
+    {
+      Templates::const_iterator t = templates.find(level);
+      if (t == templates.end())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      
+      index_.reset(new ResourcesIndex(context, level, t->second));
+      
+      if (level == ResourceType_Instance)
+      {
+        timeMetadata_ = MetadataType_Instance_ReceptionDate;
+      }
+      else
+      {
+        timeMetadata_ = MetadataType_LastUpdate;
+      }
+    }
+
+    ResourceType GetLevel() const
+    {
+      return index_->GetLevel();
+    }
+
+    const Templates& GetTemplates() const
+    {
+      return templates_;
+    }
+  };
+
+  
+
+  class OrthancWebDav::SingleDicomResource : public ListOfResources
+  {
+  private:
+    std::string  parentId_;
+    
+  protected: 
+    virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
+    {
+      try
+      {
+        GetContext().GetIndex().GetChildren(resources, parentId_);
+      }
+      catch (OrthancException&)
+      {
+        // Unknown parent resource
+        resources.clear();
+      }
+    }
+
+    virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
+    {
+      if (GetLevel() == ResourceType_Instance)
+      {
+        return NULL;
+      }
+      else if (GetLevel() == ResourceType_Series)
+      {
+        return new InstancesOfSeries(GetContext(), resource);
+      }
+      else
+      {
+        ResourceType l = GetChildResourceType(GetLevel());
+        return new SingleDicomResource(GetContext(), l, resource, GetTemplates());
+      }
+    }
+
+  public:
+    SingleDicomResource(ServerContext& context,
+                        ResourceType level,
+                        const std::string& parentId,
+                        const Templates& templates) :
+      ListOfResources(context, level, templates),
+      parentId_(parentId)
+    {
+    }
+  };
+  
+  
+  class OrthancWebDav::RootNode : public ListOfResources
+  {
+  protected:   
+    virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
+    {
+      GetContext().GetIndex().GetAllUuids(resources, GetLevel());
+    }
+
+    virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
+    {
+      if (GetLevel() == ResourceType_Series)
+      {
+        return new InstancesOfSeries(GetContext(), resource);
+      }
+      else
+      {
+        ResourceType l = GetChildResourceType(GetLevel());
+        return new SingleDicomResource(GetContext(), l, resource, GetTemplates());
+      }
+    }
+
+  public:
+    RootNode(ServerContext& context,
+             ResourceType level,
+             const Templates& templates) :
+      ListOfResources(context, level, templates)
+    {
+    }
+  };
+
+
+  class OrthancWebDav::ListOfStudiesByDate : public ListOfResources
+  {
+  private:
+    std::string  year_;
+    std::string  month_;
+
+    class Visitor : public ServerContext::ILookupVisitor
+    {
+    private:
+      std::list<std::string>&  resources_;
+
+    public:
+      Visitor(std::list<std::string>& resources) :
+        resources_(resources)
+      {
+      }
+
+      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
+      {
+        return false;   // (*)
+      }
+      
+      virtual void MarkAsComplete() ORTHANC_OVERRIDE
+      {
+      }
+
+      virtual void Visit(const std::string& publicId,
+                         const std::string& instanceId   /* unused     */,
+                         const DicomMap& mainDicomTags,
+                         const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+      {
+        resources_.push_back(publicId);
+      }
+    };
+    
+  protected:   
+    virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
+    {
+      DatabaseLookup query;
+      query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + month_ + "01-" + year_ + month_ + "31",
+                              true /* case sensitive */, true /* mandatory tag */);
+
+      Visitor visitor(resources);
+      GetContext().Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
+    }
+
+    virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
+    {
+      return new SingleDicomResource(GetContext(), ResourceType_Series, resource, GetTemplates());
+    }
+
+  public:
+    ListOfStudiesByDate(ServerContext& context,
+                        const std::string& year,
+                        const std::string& month,
+                        const Templates& templates) :
+      ListOfResources(context, ResourceType_Study, templates),
+      year_(year),
+      month_(month)
+    {
+      if (year.size() != 4 ||
+          month.size() != 2)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  };
+
+
+  class OrthancWebDav::ListOfStudiesByMonth : public InternalNode
+  {
+  private:
+    ServerContext&    context_;
+    std::string       year_;
+    const Templates&  templates_;
+
+    class Visitor : public ServerContext::ILookupVisitor
+    {
+    private:
+      std::set<std::string> months_;
+      
+    public:
+      Visitor()
+      {
+      }
+
+      const std::set<std::string>& GetMonths() const
+      {
+        return months_;
+      }
+      
+      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
+      {
+        return false;   // (*)
+      }
+      
+      virtual void MarkAsComplete() ORTHANC_OVERRIDE
+      {
+      }
+
+      virtual void Visit(const std::string& publicId,
+                         const std::string& instanceId   /* unused     */,
+                         const DicomMap& mainDicomTags,
+                         const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+      {
+        std::string s;
+        if (mainDicomTags.LookupStringValue(s, DICOM_TAG_STUDY_DATE, false) &&
+            s.size() == 8)
+        {
+          months_.insert(s.substr(4, 2)); // Get the month from "YYYYMMDD"
+        }
+      }
+    };
+
+  protected:
+    virtual void Refresh() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE
+    {
+      DatabaseLookup query;
+      query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + "0101-" + year_ + "1231",
+                              true /* case sensitive */, true /* mandatory tag */);
+
+      Visitor visitor;
+      context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
+
+      for (std::set<std::string>::const_iterator it = visitor.GetMonths().begin();
+           it != visitor.GetMonths().end(); ++it)
+      {
+        target.AddResource(new IWebDavBucket::Folder(year_ + "-" + *it));
+      }
+
+      return true;
+    }
+
+    virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE
+    {
+      if (path.size() != 7)  // Format: "YYYY-MM"
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        const std::string year = path.substr(0, 4);
+        const std::string month = path.substr(5, 2);
+        return new ListOfStudiesByDate(context_, year, month, templates_);
+      }
+    }
+
+  public:
+    ListOfStudiesByMonth(ServerContext& context,
+                         const std::string& year,
+                         const Templates& templates) :
+      context_(context),
+      year_(year),
+      templates_(templates)
+    {
+      if (year_.size() != 4)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  };
+
+  
+  class OrthancWebDav::ListOfStudiesByYear : public InternalNode
+  {
+  private:
+    ServerContext&    context_;
+    const Templates&  templates_;
+
+  protected:
+    virtual void Refresh() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE
+    {
+      std::list<std::string> resources;
+      context_.GetIndex().GetAllUuids(resources, ResourceType_Study);
+
+      std::set<std::string> years;
+      
+      for (std::list<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it)
+      {
+        DicomMap tags;
+        std::string studyDate;
+        if (context_.GetIndex().GetMainDicomTags(tags, *it, ResourceType_Study, ResourceType_Study) &&
+            tags.LookupStringValue(studyDate, DICOM_TAG_STUDY_DATE, false) &&
+            studyDate.size() == 8)
+        {
+          years.insert(studyDate.substr(0, 4)); // Get the year from "YYYYMMDD"
+        }
+      }
+      
+      for (std::set<std::string>::const_iterator it = years.begin(); it != years.end(); ++it)
+      {
+        target.AddResource(new IWebDavBucket::Folder(*it));
+      }
+
+      return true;
+    }
+
+    virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE
+    {
+      return new ListOfStudiesByMonth(context_, path, templates_);
+    }
+
+  public:
+    ListOfStudiesByYear(ServerContext& context,
+                        const Templates& templates) :
+      context_(context),
+      templates_(templates)
+    {
+    }
+  };
+
+
+  void OrthancWebDav::AddVirtualFile(Collection& collection,
+                                     const UriComponents& path,
+                                     const std::string& filename)
+  {
+    MimeType mime;
+    std::string content;
+    boost::posix_time::ptime modification;
+
+    UriComponents p = path;
+    p.push_back(filename);
+
+    if (GetFileContent(mime, content, modification, p))
+    {
+      std::unique_ptr<File> f(new File(filename));
+      f->SetMimeType(mime);
+      f->SetContentLength(content.size());
+      f->SetCreationTime(modification);
+      collection.AddResource(f.release());
+    }
+  }
+
+
+  void OrthancWebDav::UploadWorker(OrthancWebDav* that)
+  {
+    assert(that != NULL);
+
+    boost::posix_time::ptime lastModification = GetNow();
+
+    while (that->running_)
+    {
+      std::unique_ptr<IDynamicObject> obj(that->uploadQueue_.Dequeue(100));
+      if (obj.get() != NULL)
+      {
+        that->Upload(reinterpret_cast<const SingleValueObject<std::string>&>(*obj).GetValue());
+        lastModification = GetNow();
+      }
+      else if (GetNow() - lastModification > boost::posix_time::seconds(10))
+      {
+        // After every 10 seconds of inactivity, remove the empty folders
+        LOG(INFO) << "Cleaning up the empty WebDAV upload folders";
+        that->uploads_.RemoveEmptyFolders();
+        lastModification = GetNow();
+      }
+    }
+  }
+
+  
+  void OrthancWebDav::Upload(const std::string& path)
+  {
+    UriComponents uri;
+    Toolbox::SplitUriComponents(uri, path);
+        
+    LOG(INFO) << "Upload from WebDAV: " << path;
+
+    MimeType mime;
+    std::string content;
+    boost::posix_time::ptime time;
+    if (uploads_.GetFileContent(mime, content, time, uri))
+    {
+      DicomInstanceToStore instance;
+      // instance.SetOrigin(DicomInstanceOrigin_WebDav);
+      instance.SetBuffer(content.c_str(), content.size());
+
+      std::string publicId;
+      StoreStatus status = context_.Store(publicId, instance, StoreInstanceMode_Default);
+      if (status == StoreStatus_Success ||
+          status == StoreStatus_AlreadyStored)
+      {
+        LOG(INFO) << "Successfully imported DICOM instance from WebDAV: " << path << " (Orthanc ID: " << publicId << ")";
+        uploads_.DeleteItem(uri);
+      }
+      else
+      {
+        LOG(WARNING) << "Cannot import DICOM instance from WebWAV: " << path;
+      }
+    }
+  }
+
+
+  OrthancWebDav::OrthancWebDav(ServerContext& context) :
+    context_(context),
+    uploads_(false /* store uploads as temporary files */),
+    running_(false)
+  {
+    patientsTemplates_[ResourceType_Patient] = "{{PatientID}} - {{PatientName}}";
+    patientsTemplates_[ResourceType_Study] = "{{StudyDate}} - {{StudyDescription}}";
+    patientsTemplates_[ResourceType_Series] = "{{Modality}} - {{SeriesDescription}}";
+
+    studiesTemplates_[ResourceType_Study] = "{{PatientID}} - {{PatientName}} - {{StudyDescription}}";
+    studiesTemplates_[ResourceType_Series] = patientsTemplates_[ResourceType_Series];
+
+    patients_.reset(new RootNode(context, ResourceType_Patient, patientsTemplates_));
+    studies_.reset(new RootNode(context, ResourceType_Study, studiesTemplates_));
+    dates_.reset(new ListOfStudiesByYear(context, studiesTemplates_));
+  }
+
+
+  bool OrthancWebDav::IsExistingFolder(const UriComponents& path) 
+  {
+    if (path.empty())
+    {
+      return true;
+    }
+    else if (path[0] == BY_UIDS)
+    {
+      return (path.size() <= 3 &&
+              (path.size() != 3 || path[2] != "study.json"));
+    }
+    else if (path[0] == BY_PATIENTS)
+    {
+      IWebDavBucket::Collection tmp;
+      return patients_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end()));
+    }
+    else if (path[0] == BY_STUDIES)
+    {
+      IWebDavBucket::Collection tmp;
+      return studies_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end()));
+    }
+    else if (path[0] == BY_DATE)
+    {
+      IWebDavBucket::Collection tmp;
+      return dates_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end()));
+    }
+    else if (path[0] == UPLOADS)
+    {
+      return uploads_.IsExistingFolder(UriComponents(path.begin() + 1, path.end()));
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  bool OrthancWebDav::ListCollection(Collection& collection,
+                                     const UriComponents& path) 
+  {
+    if (path.empty())
+    {
+      collection.AddResource(new Folder(BY_DATE));
+      collection.AddResource(new Folder(BY_PATIENTS));
+      collection.AddResource(new Folder(BY_STUDIES));
+      collection.AddResource(new Folder(BY_UIDS));
+      collection.AddResource(new Folder(UPLOADS));
+      return true;
+    }   
+    else if (path[0] == BY_UIDS)
+    {
+      DatabaseLookup query;
+      ResourceType level;
+      size_t limit = 0;  // By default, no limits
+
+      if (path.size() == 1)
+      {
+        level = ResourceType_Study;
+        limit = 100;  // TODO
+      }
+      else if (path.size() == 2)
+      {
+        AddVirtualFile(collection, path, "study.json");
+
+        level = ResourceType_Series;
+        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
+                                true /* case sensitive */, true /* mandatory tag */);
+      }      
+      else if (path.size() == 3)
+      {
+        AddVirtualFile(collection, path, "series.json");
+
+        level = ResourceType_Instance;
+        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
+                                true /* case sensitive */, true /* mandatory tag */);
+        query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
+                                true /* case sensitive */, true /* mandatory tag */);
+      }
+      else
+      {
+        return false;
+      }
+
+      DicomIdentifiersVisitor visitor(context_, collection, level);
+      context_.Apply(visitor, query, level, 0 /* since */, limit);
+      
+      return true;
+    }
+    else if (path[0] == BY_PATIENTS)
+    {
+      return patients_->ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
+    }
+    else if (path[0] == BY_STUDIES)
+    {
+      return studies_->ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
+    }
+    else if (path[0] == BY_DATE)
+    {
+      return dates_->ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
+    }
+    else if (path[0] == UPLOADS)
+    {
+      return uploads_.ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  bool OrthancWebDav::GetFileContent(MimeType& mime,
+                                     std::string& content,
+                                     boost::posix_time::ptime& modificationTime, 
+                                     const UriComponents& path) 
+  {
+    if (path.empty())
+    {
+      return false;
+    }
+    else if (path[0] == BY_UIDS)
+    {
+      if (path.size() == 3 &&
+          path[2] == "study.json")
+      {
+        DatabaseLookup query;
+        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
+                                true /* case sensitive */, true /* mandatory tag */);
+      
+        OrthancJsonVisitor visitor(context_, content, ResourceType_Study);
+        context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
+
+        mime = MimeType_Json;
+        return visitor.IsSuccess();
+      }
+      else if (path.size() == 4 &&
+               path[3] == "series.json")
+      {
+        DatabaseLookup query;
+        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
+                                true /* case sensitive */, true /* mandatory tag */);
+        query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
+                                true /* case sensitive */, true /* mandatory tag */);
+      
+        OrthancJsonVisitor visitor(context_, content, ResourceType_Series);
+        context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */);
+
+        mime = MimeType_Json;
+        return visitor.IsSuccess();
+      }
+      else if (path.size() == 4 &&
+               boost::ends_with(path[3], ".dcm"))
+      {
+        std::string sopInstanceUid = path[3];
+        sopInstanceUid.resize(sopInstanceUid.size() - 4);
+        
+        DatabaseLookup query;
+        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
+                                true /* case sensitive */, true /* mandatory tag */);
+        query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
+                                true /* case sensitive */, true /* mandatory tag */);
+        query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid,
+                                true /* case sensitive */, true /* mandatory tag */);
+      
+        DicomFileVisitor visitor(context_, content, modificationTime);
+        context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */);
+        
+        mime = MimeType_Dicom;
+        return visitor.IsSuccess();
+      }
+      else
+      {
+        return false;
+      }
+    }
+    else if (path[0] == BY_PATIENTS)
+    {
+      return patients_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
+    }
+    else if (path[0] == BY_STUDIES)
+    {
+      return studies_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
+    }
+    else if (path[0] == UPLOADS)
+    {
+      return uploads_.GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  bool OrthancWebDav::StoreFile(const std::string& content,
+                                const UriComponents& path) 
+  {
+    if (path.size() >= 1 &&
+        path[0] == UPLOADS)
+    {
+      UriComponents subpath(UriComponents(path.begin() + 1, path.end()));
+
+      if (uploads_.StoreFile(content, subpath))
+      {
+        if (!content.empty())
+        {
+          uploadQueue_.Enqueue(new SingleValueObject<std::string>(Toolbox::FlattenUri(subpath)));
+        }
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool OrthancWebDav::CreateFolder(const UriComponents& path)
+  {
+    if (path.size() >= 1 &&
+        path[0] == UPLOADS)
+    {
+      return uploads_.CreateFolder(UriComponents(path.begin() + 1, path.end()));
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  
+  bool OrthancWebDav::DeleteItem(const std::vector<std::string>& path) 
+  {
+    if (path.size() >= 1 &&
+        path[0] == UPLOADS)
+    {
+      return uploads_.DeleteItem(UriComponents(path.begin() + 1, path.end()));
+    }
+    else
+    {
+      return false;  // read-only
+    }
+  }
+
+  
+  void OrthancWebDav::Start() 
+  {
+    if (running_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      LOG(INFO) << "Starting the WebDAV upload thread";
+      running_ = true;
+      uploadThread_ = boost::thread(UploadWorker, this);
+    }
+  }
+
+  
+  void OrthancWebDav::Stop() 
+  {
+    if (running_)
+    {
+      LOG(INFO) << "Stopping the WebDAV upload thread";
+      running_ = false;
+      if (uploadThread_.joinable())
+      {
+        uploadThread_.join();
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancWebDav.h	Fri Oct 09 12:02:40 2020 +0200
@@ -0,0 +1,128 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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 "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h"
+#include "../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h"
+#include "../../OrthancFramework/Sources/Toolbox.h"
+
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class OrthancWebDav : public IWebDavBucket
+  {
+  private:
+    typedef std::map<ResourceType, std::string>  Templates;
+
+    class DicomIdentifiersVisitor;  
+    class DicomFileVisitor;
+    class OrthancJsonVisitor;
+    class InstancesOfSeries;
+    class InternalNode;
+    class ListOfResources;
+    class ListOfStudiesByDate;
+    class ListOfStudiesByMonth;
+    class ListOfStudiesByYear;
+    class ResourcesIndex;
+    class RootNode;
+    class SingleDicomResource;
+    
+    class INode : public boost::noncopyable
+    {
+    public:
+      virtual ~INode()
+      {
+      }
+
+      virtual bool ListCollection(IWebDavBucket::Collection& target,
+                                  const UriComponents& path) = 0;
+
+      virtual bool GetFileContent(MimeType& mime,
+                                  std::string& content,
+                                  boost::posix_time::ptime& time, 
+                                  const UriComponents& path) = 0;
+    };
+
+
+    void AddVirtualFile(Collection& collection,
+                        const UriComponents& path,
+                        const std::string& filename);
+    
+    static void UploadWorker(OrthancWebDav* that);
+
+    void Upload(const std::string& path);
+  
+    ServerContext&          context_;
+    std::unique_ptr<INode>  patients_;
+    std::unique_ptr<INode>  studies_;
+    std::unique_ptr<INode>  dates_;
+    Templates               patientsTemplates_;
+    Templates               studiesTemplates_;
+    WebDavStorage           uploads_;
+    SharedMessageQueue      uploadQueue_;
+    boost::thread           uploadThread_;
+    bool                    running_;
+  
+  public:
+    OrthancWebDav(ServerContext& context);
+
+    virtual ~OrthancWebDav()
+    {
+      Stop();
+    }
+
+    virtual bool IsExistingFolder(const UriComponents& path) ORTHANC_OVERRIDE;
+
+    virtual bool ListCollection(Collection& collection,
+                                const UriComponents& path) ORTHANC_OVERRIDE;
+
+    virtual bool GetFileContent(MimeType& mime,
+                                std::string& content,
+                                boost::posix_time::ptime& modificationTime, 
+                                const UriComponents& path) ORTHANC_OVERRIDE;
+  
+    virtual bool StoreFile(const std::string& content,
+                           const UriComponents& path) ORTHANC_OVERRIDE;
+
+    virtual bool CreateFolder(const UriComponents& path) ORTHANC_OVERRIDE;
+
+    virtual bool DeleteItem(const std::vector<std::string>& path) ORTHANC_OVERRIDE;
+
+    virtual void Start() ORTHANC_OVERRIDE;
+
+    virtual void Stop() ORTHANC_OVERRIDE;
+  };
+}
--- a/OrthancServer/Sources/main.cpp	Fri Oct 09 11:38:03 2020 +0200
+++ b/OrthancServer/Sources/main.cpp	Fri Oct 09 12:02:40 2020 +0200
@@ -34,8 +34,6 @@
 #include "PrecompiledHeadersServer.h"
 #include "OrthancRestApi/OrthancRestApi.h"
 
-#include <boost/algorithm/string/predicate.hpp>
-
 #include "../../OrthancFramework/Sources/Compatibility.h"
 #include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
 #include "../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h"
@@ -52,14 +50,13 @@
 #include "OrthancGetRequestHandler.h"
 #include "OrthancInitialization.h"
 #include "OrthancMoveRequestHandler.h"
+#include "OrthancWebDav.h"
 #include "ServerContext.h"
 #include "ServerJobs/StorageCommitmentScpJob.h"
 #include "ServerToolbox.h"
 #include "StorageCommitmentReports.h"
 
-#include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h"  // TODO
-#include "Search/DatabaseLookup.h"  // TODO
-#include <boost/regex.hpp> // TODO
+#include <boost/algorithm/string/predicate.hpp>
 
 
 using namespace Orthanc;
@@ -615,1603 +612,6 @@
 };
 
 
-
-
-
-
-
-static const char* const UPLOAD_FOLDER = "upload";
-
-class DummyBucket : public IWebDavBucket  // TODO
-{
-private:
-  ServerContext&  context_;
-  WebDavStorage   storage_;
-
-  bool IsUploadedFolder(const UriComponents& path) const
-  {
-    return (path.size() >= 1 && path[0] == UPLOAD_FOLDER);
-  }
-
-public:
-  DummyBucket(ServerContext& context,
-              bool isMemory) :
-    context_(context),
-    storage_(isMemory)
-  {
-  }
-  
-  virtual bool IsExistingFolder(const UriComponents& path) ORTHANC_OVERRIDE
-  {
-    if (IsUploadedFolder(path))
-    {
-      return storage_.IsExistingFolder(UriComponents(path.begin() + 1, path.end()));
-    }
-    else
-    {
-      return (path.size() == 0 ||
-              (path.size() == 1 && path[0] == "Folder1") ||
-              (path.size() == 2 && path[0] == "Folder1" && path[1] == "Folder2"));
-    }
-  }
-
-  virtual bool ListCollection(Collection& collection,
-                              const UriComponents& path) ORTHANC_OVERRIDE
-  {
-    if (IsUploadedFolder(path))
-    {
-      return storage_.ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (IsExistingFolder(path))
-    {
-      if (path.empty())
-      {
-        collection.AddResource(new Folder(UPLOAD_FOLDER));
-      }
-      
-      for (unsigned int i = 0; i < 5; i++)
-      {
-        std::unique_ptr<File> f(new File("IM" + boost::lexical_cast<std::string>(i) + ".dcm"));
-        f->SetContentLength(1024 * i);
-        f->SetMimeType(MimeType_PlainText);
-        collection.AddResource(f.release());
-      }
-        
-      for (unsigned int i = 0; i < 5; i++)
-      {
-        collection.AddResource(new Folder("Folder" + boost::lexical_cast<std::string>(i)));
-      }
-
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  virtual bool GetFileContent(MimeType& mime,
-                              std::string& content,
-                              boost::posix_time::ptime& time, 
-                              const UriComponents& path) ORTHANC_OVERRIDE
-  {
-    if (path.empty())
-    {
-      return false;
-    }
-    else if (IsUploadedFolder(path))
-    {
-      return storage_.GetFileContent(mime, content, time,
-                                     UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (path.back() == "IM0.dcm" ||
-             path.back() == "IM1.dcm" ||
-             path.back() == "IM2.dcm" ||
-             path.back() == "IM3.dcm" ||
-             path.back() == "IM4.dcm")
-    {
-      time = boost::posix_time::second_clock::universal_time();
-
-      std::string s;
-      for (size_t i = 0; i < path.size(); i++)
-      {
-        s += "/" + path[i];
-      }
-      
-      content = "Hello world!\r\n" + s + "\r\n";
-      mime = MimeType_PlainText;
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  
-  virtual bool StoreFile(const std::string& content,
-                         const UriComponents& path) ORTHANC_OVERRIDE
-  {
-    if (IsUploadedFolder(path))
-    {
-      return storage_.StoreFile(content, UriComponents(path.begin() + 1, path.end()));
-    }
-    else
-    {
-      LOG(WARNING) << "Writing to a read-only location in WebDAV: " << Toolbox::FlattenUri(path);
-      return false;
-    }
-  }
-
-
-  virtual bool CreateFolder(const UriComponents& path) ORTHANC_OVERRIDE
-  {
-    if (IsUploadedFolder(path))
-    {
-      return storage_.CreateFolder(UriComponents(path.begin() + 1, path.end()));
-    }
-    else
-    {
-      LOG(WARNING) << "Writing to a read-only location in WebDAV: " << Toolbox::FlattenUri(path);
-      return false;
-    }
-  }
-
-  virtual bool DeleteItem(const std::vector<std::string>& path) ORTHANC_OVERRIDE
-  {
-    return false;  // read-only
-  }
-
-  virtual void Start() ORTHANC_OVERRIDE
-  {
-    LOG(WARNING) << "Starting WebDAV";
-  }
-
-  virtual void Stop() ORTHANC_OVERRIDE
-  {
-    LOG(WARNING) << "Stopping WebDAV";
-  }
-};
-
-
-
-
-
-static const char* const BY_PATIENTS = "by-patients";
-static const char* const BY_STUDIES = "by-studies";
-static const char* const BY_DATE = "by-dates";
-static const char* const BY_UIDS = "by-uids";
-static const char* const UPLOADS = "uploads";
-static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
-
-class DummyBucket2 : public IWebDavBucket  // TODO
-{
-private:
-  typedef std::map<ResourceType, std::string>  Templates;
-
-
-  static boost::posix_time::ptime GetNow()
-  {
-    return boost::posix_time::second_clock::universal_time();
-  }
-  
-
-  static void LookupTime(boost::posix_time::ptime& target,
-                         ServerContext& context,
-                         const std::string& publicId,
-                         MetadataType metadata)
-  {
-    std::string value;
-    if (context.GetIndex().LookupMetadata(value, publicId, metadata))
-    {
-      try
-      {
-        target = boost::posix_time::from_iso_string(value);
-        return;
-      }
-      catch (std::exception& e)
-      {
-      }
-    }
-
-    target = GetNow();
-  }
-
-  
-  class DicomIdentifiersVisitor : public ServerContext::ILookupVisitor
-  {
-  private:
-    ServerContext&  context_;
-    bool            isComplete_;
-    Collection&     target_;
-    ResourceType    level_;
-
-  public:
-    DicomIdentifiersVisitor(ServerContext& context,
-                            Collection&  target,
-                            ResourceType level) :
-      context_(context),
-      isComplete_(false),
-      target_(target),
-      level_(level)
-    {
-    }
-      
-    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-    {
-      return false;   // (*)
-    }
-      
-    virtual void MarkAsComplete() ORTHANC_OVERRIDE
-    {
-      isComplete_ = true;  // TODO
-    }
-
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId   /* unused     */,
-                       const DicomMap& mainDicomTags,
-                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
-    {
-      DicomTag tag(0, 0);
-      MetadataType timeMetadata;
-
-      switch (level_)
-      {
-        case ResourceType_Study:
-          tag = DICOM_TAG_STUDY_INSTANCE_UID;
-          timeMetadata = MetadataType_LastUpdate;
-          break;
-
-        case ResourceType_Series:
-          tag = DICOM_TAG_SERIES_INSTANCE_UID;
-          timeMetadata = MetadataType_LastUpdate;
-          break;
-        
-        case ResourceType_Instance:
-          tag = DICOM_TAG_SOP_INSTANCE_UID;
-          timeMetadata = MetadataType_Instance_ReceptionDate;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-        
-      std::string s;
-      if (mainDicomTags.LookupStringValue(s, tag, false) &&
-          !s.empty())
-      {
-        std::unique_ptr<Resource> resource;
-
-        if (level_ == ResourceType_Instance)
-        {
-          FileInfo info;
-          if (context_.GetIndex().LookupAttachment(info, publicId, FileContentType_Dicom))
-          {
-            std::unique_ptr<File> f(new File(s + ".dcm"));
-            f->SetMimeType(MimeType_Dicom);
-            f->SetContentLength(info.GetUncompressedSize());
-            resource.reset(f.release());
-          }
-        }
-        else
-        {
-          resource.reset(new Folder(s));
-        }
-
-        if (resource.get() != NULL)
-        {
-          boost::posix_time::ptime t;
-          LookupTime(t, context_, publicId, timeMetadata);
-          resource->SetCreationTime(t);
-          target_.AddResource(resource.release());
-        }
-      }
-    }
-  };
-  
-  class DicomFileVisitor : public ServerContext::ILookupVisitor
-  {
-  private:
-    ServerContext&  context_;
-    bool            success_;
-    std::string&    target_;
-    boost::posix_time::ptime&  time_;
-
-  public:
-    DicomFileVisitor(ServerContext& context,
-                     std::string& target,
-                     boost::posix_time::ptime& time) :
-      context_(context),
-      success_(false),
-      target_(target),
-      time_(time)
-    {
-    }
-
-    bool IsSuccess() const
-    {
-      return success_;
-    }
-
-    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-    {
-      return false;   // (*)
-    }
-      
-    virtual void MarkAsComplete() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId   /* unused     */,
-                       const DicomMap& mainDicomTags,
-                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
-    {
-      if (success_)
-      {
-        success_ = false;  // Two matches => Error
-      }
-      else
-      {
-        LookupTime(time_, context_, publicId, MetadataType_Instance_ReceptionDate);
-        context_.ReadDicom(target_, publicId);
-        success_ = true;
-      }
-    }
-  };
-  
-  class OrthancJsonVisitor : public ServerContext::ILookupVisitor
-  {
-  private:
-    ServerContext&  context_;
-    bool            success_;
-    std::string&    target_;
-    ResourceType    level_;
-
-  public:
-    OrthancJsonVisitor(ServerContext& context,
-                       std::string& target,
-                       ResourceType level) :
-      context_(context),
-      success_(false),
-      target_(target),
-      level_(level)
-    {
-    }
-
-    bool IsSuccess() const
-    {
-      return success_;
-    }
-
-    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-    {
-      return false;   // (*)
-    }
-      
-    virtual void MarkAsComplete() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId   /* unused     */,
-                       const DicomMap& mainDicomTags,
-                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
-    {
-      Json::Value info;
-      if (context_.GetIndex().LookupResource(info, publicId, level_))
-      {
-        if (success_)
-        {
-          success_ = false;  // Two matches => Error
-        }
-        else
-        {
-          target_ = info.toStyledString();
-
-          // Replace UNIX newlines with DOS newlines 
-          boost::replace_all(target_, "\n", "\r\n");
-
-          success_ = true;
-        }
-      }
-    }
-  };
-
-
-  void AddVirtualFile(Collection& collection,
-                      const UriComponents& path,
-                      const std::string& filename)
-  {
-    MimeType mime;
-    std::string content;
-    boost::posix_time::ptime modification;
-
-    UriComponents p = path;
-    p.push_back(filename);
-
-    if (GetFileContent(mime, content, modification, p))
-    {
-      std::unique_ptr<File> f(new File(filename));
-      f->SetMimeType(mime);
-      f->SetContentLength(content.size());
-      f->SetCreationTime(modification);
-      collection.AddResource(f.release());
-    }
-  }
-
-
-
-
-  class ResourcesIndex : public boost::noncopyable
-  {
-  public:
-    typedef std::map<std::string, std::string>   Map;
-
-  private:
-    ServerContext&  context_;
-    ResourceType    level_;
-    std::string     template_;
-    Map             pathToResource_;
-    Map             resourceToPath_;
-
-    void CheckInvariants()
-    {
-#ifndef NDEBUG
-      assert(pathToResource_.size() == resourceToPath_.size());
-
-      for (Map::const_iterator it = pathToResource_.begin(); it != pathToResource_.end(); ++it)
-      {
-        assert(resourceToPath_[it->second] == it->first);
-      }
-
-      for (Map::const_iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it)
-      {
-        assert(pathToResource_[it->second] == it->first);
-      }
-#endif
-    }      
-
-    void AddTags(DicomMap& target,
-                 const std::string& resourceId,
-                 ResourceType tagsFromLevel)
-    {
-      DicomMap tags;
-      if (context_.GetIndex().GetMainDicomTags(tags, resourceId, level_, tagsFromLevel))
-      {
-        target.Merge(tags);
-      }
-    }
-
-    void Register(const std::string& resourceId)
-    {
-      // Don't register twice the same resource
-      if (resourceToPath_.find(resourceId) == resourceToPath_.end())
-      {
-        std::string name = template_;
-
-        DicomMap tags;
-
-        AddTags(tags, resourceId, level_);
-        
-        if (level_ == ResourceType_Study)
-        {
-          AddTags(tags, resourceId, ResourceType_Patient);
-        }
-        
-        DicomArray arr(tags);
-        for (size_t i = 0; i < arr.GetSize(); i++)
-        {
-          const DicomElement& element = arr.GetElement(i);
-          if (!element.GetValue().IsNull() &&
-              !element.GetValue().IsBinary())
-          {
-            const std::string tag = FromDcmtkBridge::GetTagName(element.GetTag(), "");
-            boost::replace_all(name, "{{" + tag + "}}", element.GetValue().GetContent());
-          } 
-        }
-
-        // Blank the tags that were not matched
-        static const boost::regex REGEX_BLANK_TAGS("{{.*?}}");  // non-greedy match
-        name = boost::regex_replace(name, REGEX_BLANK_TAGS, "");
-
-        // UTF-8 characters cannot be used on Windows XP
-        name = Toolbox::ConvertToAscii(name);
-        boost::replace_all(name, "/", "");
-        boost::replace_all(name, "\\", "");
-
-        // Trim sequences of spaces as one single space
-        static const boost::regex REGEX_TRIM_SPACES("{{.*?}}");
-        name = boost::regex_replace(name, REGEX_TRIM_SPACES, " ");
-        name = Toolbox::StripSpaces(name);
-
-        size_t count = 0;
-        for (;;)
-        {
-          std::string path = name;
-          if (count > 0)
-          {
-            path += " (" + boost::lexical_cast<std::string>(count) + ")";
-          }
-
-          if (pathToResource_.find(path) == pathToResource_.end())
-          {
-            pathToResource_[path] = resourceId;
-            resourceToPath_[resourceId] = path;
-            return;
-          }
-
-          count++;
-        }
-
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-  public:
-    ResourcesIndex(ServerContext& context,
-                   ResourceType level,
-                   const std::string& templateString) :
-      context_(context),
-      level_(level),
-      template_(templateString)
-    {
-    }
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    void Refresh(std::set<std::string>& removedPaths /* out */,
-                 const std::set<std::string>& resources)
-    {
-      CheckInvariants();
-
-      // Detect the resources that have been removed since last refresh
-      removedPaths.clear();
-      std::set<std::string> removedResources;
-      
-      for (Map::iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it)
-      {
-        if (resources.find(it->first) == resources.end())
-        {
-          const std::string& path = it->second;
-          
-          assert(pathToResource_.find(path) != pathToResource_.end());
-          pathToResource_.erase(path);
-          removedPaths.insert(path);
-          
-          removedResources.insert(it->first);  // Delay the removal to avoid disturbing the iterator
-        }
-      }
-
-      // Remove the missing resources
-      for (std::set<std::string>::const_iterator it = removedResources.begin(); it != removedResources.end(); ++it)
-      {
-        assert(resourceToPath_.find(*it) != resourceToPath_.end());
-        resourceToPath_.erase(*it);
-      }
-
-      CheckInvariants();
-
-      for (std::set<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it)
-      {
-        Register(*it);
-      }
-
-      CheckInvariants();
-    }
-
-    const Map& GetPathToResource() const
-    {
-      return pathToResource_;
-    }
-  };
-
-
-  class INode : public boost::noncopyable
-  {
-  public:
-    virtual ~INode()
-    {
-    }
-
-    virtual bool ListCollection(IWebDavBucket::Collection& target,
-                                const UriComponents& path) = 0;
-
-    virtual bool GetFileContent(MimeType& mime,
-                                std::string& content,
-                                boost::posix_time::ptime& time, 
-                                const UriComponents& path) = 0;
-  };
-
-
-  
-  class InstancesOfSeries : public INode
-  {
-  private:
-    ServerContext&  context_;
-    std::string     parentSeries_;
-
-  public:
-    InstancesOfSeries(ServerContext& context,
-                      const std::string& parentSeries) :
-      context_(context),
-      parentSeries_(parentSeries)
-    {
-    }
-
-    virtual bool ListCollection(IWebDavBucket::Collection& target,
-                                const UriComponents& path) ORTHANC_OVERRIDE
-    {
-      if (path.empty())
-      {
-        std::list<std::string> resources;
-        try
-        {
-          context_.GetIndex().GetChildren(resources, parentSeries_);
-        }
-        catch (OrthancException&)
-        {
-          // Unknown (or deleted) parent series
-          return false;
-        }
-
-        for (std::list<std::string>::const_iterator
-               it = resources.begin(); it != resources.end(); ++it)
-        {
-          boost::posix_time::ptime time;
-          LookupTime(time, context_, *it, MetadataType_Instance_ReceptionDate);
-
-          FileInfo info;
-          if (context_.GetIndex().LookupAttachment(info, *it, FileContentType_Dicom))
-          {
-            std::unique_ptr<File> resource(new File(*it + ".dcm"));
-            resource->SetMimeType(MimeType_Dicom);
-            resource->SetContentLength(info.GetUncompressedSize());
-            resource->SetCreationTime(time);
-            target.AddResource(resource.release());
-          }          
-        }
-        
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-    virtual bool GetFileContent(MimeType& mime,
-                                std::string& content,
-                                boost::posix_time::ptime& time, 
-                                const UriComponents& path) ORTHANC_OVERRIDE
-    {
-      if (path.size() == 1 &&
-          boost::ends_with(path[0], ".dcm"))
-      {
-        std::string instanceId = path[0].substr(0, path[0].size() - 4);
-
-        try
-        {
-          mime = MimeType_Dicom;
-          context_.ReadDicom(content, instanceId);
-          LookupTime(time, context_, instanceId, MetadataType_Instance_ReceptionDate);
-          return true;
-        }
-        catch (OrthancException&)
-        {
-          // File was removed
-          return false;
-        }
-      }
-      else
-      {
-        return false;
-      }
-    }
-  };
-
-
-
-  /**
-   * The "InternalNode" class corresponds to a non-leaf node in the
-   * WebDAV tree, that only contains subfolders (no file).
-   * 
-   * TODO: Implement a LRU index to dynamically remove the oldest
-   * children on high RAM usage.
-   **/
-  class InternalNode : public INode
-  {
-  private:
-    typedef std::map<std::string, INode*>  Children;
-
-    Children  children_;
-
-    INode* GetChild(const std::string& path)  // Don't delete the result pointer!
-    {
-      Children::const_iterator child = children_.find(path);
-      if (child == children_.end())
-      {
-        INode* child = CreateChild(path);
-        
-        if (child == NULL)
-        {
-          return NULL;
-        }
-        else
-        {
-          children_[path] = child;
-          return child;
-        }
-      }
-      else
-      {
-        assert(child->second != NULL);
-        return child->second;
-      }
-    }
-
-  protected:
-    void RemoveSubfolder(const std::string& path)
-    {
-      Children::iterator child = children_.find(path);
-      if (child != children_.end())
-      {
-        assert(child->second != NULL);
-        delete child->second;
-        children_.erase(child);
-      }
-    }
-    
-    virtual void Refresh() = 0;
-    
-    virtual bool ListSubfolders(IWebDavBucket::Collection& target) = 0;
-
-    virtual INode* CreateChild(const std::string& path) = 0;
-
-  public:
-    virtual ~InternalNode()
-    {
-      for (Children::iterator it = children_.begin(); it != children_.end(); ++it)
-      {
-        assert(it->second != NULL);
-        delete it->second;
-      }
-    }
-
-    virtual bool ListCollection(IWebDavBucket::Collection& target,
-                                const UriComponents& path)
-      ORTHANC_OVERRIDE ORTHANC_FINAL
-    {
-      Refresh();
-      
-      if (path.empty())
-      {
-        return ListSubfolders(target);
-      }
-      else
-      {
-        // Recursivity
-        INode* child = GetChild(path[0]);
-        if (child == NULL)
-        {
-          return false;
-        }
-        else
-        {
-          UriComponents subpath(path.begin() + 1, path.end());
-          return child->ListCollection(target, subpath);
-        }
-      }
-    }
-
-    virtual bool GetFileContent(MimeType& mime,
-                                std::string& content,
-                                boost::posix_time::ptime& time, 
-                                const UriComponents& path)
-      ORTHANC_OVERRIDE ORTHANC_FINAL
-    {
-      if (path.empty())
-      {
-        return false;  // An internal node doesn't correspond to a file
-      }
-      else
-      {
-        // Recursivity
-        Refresh();
-      
-        INode* child = GetChild(path[0]);
-        if (child == NULL)
-        {
-          return false;
-        }
-        else
-        {
-          UriComponents subpath(path.begin() + 1, path.end());
-          return child->GetFileContent(mime, content, time, subpath);
-        }
-      }
-    }
-  };
-  
-
-  class ListOfResources : public InternalNode
-  {
-  private:
-    ServerContext&                   context_;
-    const Templates&                 templates_;
-    std::unique_ptr<ResourcesIndex>  index_;
-    MetadataType                     timeMetadata_;
-
-  protected:
-    virtual void Refresh() ORTHANC_OVERRIDE ORTHANC_FINAL
-    {
-      std::list<std::string> resources;
-      GetCurrentResources(resources);
-
-      std::set<std::string> removedPaths;
-      index_->Refresh(removedPaths, std::set<std::string>(resources.begin(), resources.end()));
-
-      // Remove the children whose associated resource doesn't exist anymore
-      for (std::set<std::string>::const_iterator
-             it = removedPaths.begin(); it != removedPaths.end(); ++it)
-      {
-        RemoveSubfolder(*it);
-      }
-    }
-
-    virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE ORTHANC_FINAL
-    {
-      if (index_->GetLevel() == ResourceType_Instance)
-      {
-        // Not a collection, no subfolders
-        return false;
-      }
-      else
-      {
-        const ResourcesIndex::Map& paths = index_->GetPathToResource();
-        
-        for (ResourcesIndex::Map::const_iterator it = paths.begin(); it != paths.end(); ++it)
-        {
-          boost::posix_time::ptime time;
-          LookupTime(time, context_, it->second, timeMetadata_);
-
-          std::unique_ptr<IWebDavBucket::Resource> resource(new IWebDavBucket::Folder(it->first));
-          resource->SetCreationTime(time);
-          target.AddResource(resource.release());
-        }
-
-        return true;
-      }
-    }
-
-    virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE ORTHANC_FINAL
-    {
-      ResourcesIndex::Map::const_iterator resource = index_->GetPathToResource().find(path);
-      if (resource == index_->GetPathToResource().end())
-      {
-        return NULL;
-      }
-      else
-      {
-        return CreateResourceNode(resource->second);
-      }
-    }
-
-    ServerContext& GetContext() const
-    {
-      return context_;
-    }
-    
-    virtual void GetCurrentResources(std::list<std::string>& resources) = 0;
-
-    virtual INode* CreateResourceNode(const std::string& resource) = 0;
-    
-  public:
-    ListOfResources(ServerContext& context,
-                    ResourceType level,
-                    const Templates& templates) :
-      context_(context),
-      templates_(templates)
-    {
-      Templates::const_iterator t = templates.find(level);
-      if (t == templates.end())
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      
-      index_.reset(new ResourcesIndex(context, level, t->second));
-      
-      if (level == ResourceType_Instance)
-      {
-        timeMetadata_ = MetadataType_Instance_ReceptionDate;
-      }
-      else
-      {
-        timeMetadata_ = MetadataType_LastUpdate;
-      }
-    }
-
-    ResourceType GetLevel() const
-    {
-      return index_->GetLevel();
-    }
-
-    const Templates& GetTemplates() const
-    {
-      return templates_;
-    }
-  };
-
-  
-
-  class SingleDicomResource : public ListOfResources
-  {
-  private:
-    std::string  parentId_;
-    
-  protected: 
-    virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
-    {
-      try
-      {
-        GetContext().GetIndex().GetChildren(resources, parentId_);
-      }
-      catch (OrthancException&)
-      {
-        // Unknown parent resource
-        resources.clear();
-      }
-    }
-
-    virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
-    {
-      if (GetLevel() == ResourceType_Instance)
-      {
-        return NULL;
-      }
-      else if (GetLevel() == ResourceType_Series)
-      {
-        return new InstancesOfSeries(GetContext(), resource);
-      }
-      else
-      {
-        ResourceType l = GetChildResourceType(GetLevel());
-        return new SingleDicomResource(GetContext(), l, resource, GetTemplates());
-      }
-    }
-
-  public:
-    SingleDicomResource(ServerContext& context,
-                  ResourceType level,
-                  const std::string& parentId,
-                  const Templates& templates) :
-      ListOfResources(context, level, templates),
-      parentId_(parentId)
-    {
-    }
-  };
-  
-  
-  class RootNode : public ListOfResources
-  {
-  protected:   
-    virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
-    {
-      GetContext().GetIndex().GetAllUuids(resources, GetLevel());
-    }
-
-    virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
-    {
-      if (GetLevel() == ResourceType_Series)
-      {
-        return new InstancesOfSeries(GetContext(), resource);
-      }
-      else
-      {
-        ResourceType l = GetChildResourceType(GetLevel());
-        return new SingleDicomResource(GetContext(), l, resource, GetTemplates());
-      }
-    }
-
-  public:
-    RootNode(ServerContext& context,
-             ResourceType level,
-             const Templates& templates) :
-      ListOfResources(context, level, templates)
-    {
-    }
-  };
-
-
-  class ListOfStudiesByDate : public ListOfResources
-  {
-  private:
-    std::string  year_;
-    std::string  month_;
-
-    class Visitor : public ServerContext::ILookupVisitor
-    {
-    private:
-      std::list<std::string>&  resources_;
-
-    public:
-      Visitor(std::list<std::string>& resources) :
-        resources_(resources)
-      {
-      }
-
-      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-      {
-        return false;   // (*)
-      }
-      
-      virtual void MarkAsComplete() ORTHANC_OVERRIDE
-      {
-      }
-
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId   /* unused     */,
-                         const DicomMap& mainDicomTags,
-                         const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
-      {
-        resources_.push_back(publicId);
-      }
-    };
-    
-  protected:   
-    virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
-    {
-      DatabaseLookup query;
-      query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + month_ + "01-" + year_ + month_ + "31",
-                              true /* case sensitive */, true /* mandatory tag */);
-
-      Visitor visitor(resources);
-      GetContext().Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
-    }
-
-    virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
-    {
-      return new SingleDicomResource(GetContext(), ResourceType_Series, resource, GetTemplates());
-    }
-
-  public:
-    ListOfStudiesByDate(ServerContext& context,
-                        const std::string& year,
-                        const std::string& month,
-                        const Templates& templates) :
-      ListOfResources(context, ResourceType_Study, templates),
-      year_(year),
-      month_(month)
-    {
-      if (year.size() != 4 ||
-          month.size() != 2)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-  };
-
-
-  class ListOfStudiesByMonth : public InternalNode
-  {
-  private:
-    ServerContext&    context_;
-    std::string       year_;
-    const Templates&  templates_;
-
-    class Visitor : public ServerContext::ILookupVisitor
-    {
-    private:
-      std::set<std::string> months_;
-      
-    public:
-      Visitor()
-      {
-      }
-
-      const std::set<std::string>& GetMonths() const
-      {
-        return months_;
-      }
-      
-      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-      {
-        return false;   // (*)
-      }
-      
-      virtual void MarkAsComplete() ORTHANC_OVERRIDE
-      {
-      }
-
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId   /* unused     */,
-                         const DicomMap& mainDicomTags,
-                         const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
-      {
-        std::string s;
-        if (mainDicomTags.LookupStringValue(s, DICOM_TAG_STUDY_DATE, false) &&
-            s.size() == 8)
-        {
-          months_.insert(s.substr(4, 2)); // Get the month from "YYYYMMDD"
-        }
-      }
-    };
-
-  protected:
-    virtual void Refresh() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE
-    {
-      DatabaseLookup query;
-      query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + "0101-" + year_ + "1231",
-                              true /* case sensitive */, true /* mandatory tag */);
-
-      Visitor visitor;
-      context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
-
-      for (std::set<std::string>::const_iterator it = visitor.GetMonths().begin();
-           it != visitor.GetMonths().end(); ++it)
-      {
-        target.AddResource(new IWebDavBucket::Folder(year_ + "-" + *it));
-      }
-
-      return true;
-    }
-
-    virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE
-    {
-      if (path.size() != 7)  // Format: "YYYY-MM"
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-      else
-      {
-        const std::string year = path.substr(0, 4);
-        const std::string month = path.substr(5, 2);
-        return new ListOfStudiesByDate(context_, year, month, templates_);
-      }
-    }
-
-  public:
-    ListOfStudiesByMonth(ServerContext& context,
-                         const std::string& year,
-                         const Templates& templates) :
-      context_(context),
-      year_(year),
-      templates_(templates)
-    {
-      if (year_.size() != 4)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-  };
-
-  
-  class ListOfStudiesByYear : public InternalNode
-  {
-  private:
-    ServerContext&    context_;
-    const Templates&  templates_;
-
-  protected:
-    virtual void Refresh() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE
-    {
-      std::list<std::string> resources;
-      context_.GetIndex().GetAllUuids(resources, ResourceType_Study);
-
-      std::set<std::string> years;
-      
-      for (std::list<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it)
-      {
-        DicomMap tags;
-        std::string studyDate;
-        if (context_.GetIndex().GetMainDicomTags(tags, *it, ResourceType_Study, ResourceType_Study) &&
-            tags.LookupStringValue(studyDate, DICOM_TAG_STUDY_DATE, false) &&
-            studyDate.size() == 8)
-        {
-          years.insert(studyDate.substr(0, 4)); // Get the year from "YYYYMMDD"
-        }
-      }
-      
-      for (std::set<std::string>::const_iterator it = years.begin(); it != years.end(); ++it)
-      {
-        target.AddResource(new IWebDavBucket::Folder(*it));
-      }
-
-      return true;
-    }
-
-    virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE
-    {
-      return new ListOfStudiesByMonth(context_, path, templates_);
-    }
-
-  public:
-    ListOfStudiesByYear(ServerContext& context,
-                        const Templates& templates) :
-      context_(context),
-      templates_(templates)
-    {
-    }
-  };
-
-
-  static void UploadWorker(DummyBucket2* that)
-  {
-    assert(that != NULL);
-
-    boost::posix_time::ptime lastModification = GetNow();
-
-    while (that->running_)
-    {
-      std::unique_ptr<IDynamicObject> obj(that->uploadQueue_.Dequeue(100));
-      if (obj.get() != NULL)
-      {
-        that->Upload(reinterpret_cast<const SingleValueObject<std::string>&>(*obj).GetValue());
-        lastModification = GetNow();
-      }
-      else if (GetNow() - lastModification > boost::posix_time::seconds(10))
-      {
-        // After every 10 seconds of inactivity, remove the empty folders
-        LOG(INFO) << "Cleaning up the empty WebDAV upload folders";
-        that->uploads_.RemoveEmptyFolders();
-        lastModification = GetNow();
-      }
-    }
-  }
-
-  void Upload(const std::string& path)
-  {
-    UriComponents uri;
-    Toolbox::SplitUriComponents(uri, path);
-        
-    LOG(INFO) << "Upload from WebDAV: " << path;
-
-    MimeType mime;
-    std::string content;
-    boost::posix_time::ptime time;
-    if (uploads_.GetFileContent(mime, content, time, uri))
-    {
-      DicomInstanceToStore instance;
-      // instance.SetOrigin(DicomInstanceOrigin_WebDav);
-      instance.SetBuffer(content.c_str(), content.size());
-
-      std::string publicId;
-      StoreStatus status = context_.Store(publicId, instance, StoreInstanceMode_Default);
-      if (status == StoreStatus_Success ||
-          status == StoreStatus_AlreadyStored)
-      {
-        LOG(INFO) << "Successfully imported DICOM instance from WebDAV: " << path << " (Orthanc ID: " << publicId << ")";
-        uploads_.DeleteItem(uri);
-      }
-      else
-      {
-        LOG(WARNING) << "Cannot import DICOM instance from WebWAV: " << path;
-      }
-    }
-  }
-
-  
-  ServerContext&          context_;
-  std::unique_ptr<INode>  patients_;
-  std::unique_ptr<INode>  studies_;
-  std::unique_ptr<INode>  dates_;
-  Templates               patientsTemplates_;
-  Templates               studiesTemplates_;
-  WebDavStorage           uploads_;
-  SharedMessageQueue      uploadQueue_;
-  boost::thread           uploadThread_;
-  bool                    running_;
-  
-public:
-  DummyBucket2(ServerContext& context) :
-    context_(context),
-    uploads_(false /* store uploads as temporary files */),
-    running_(false)
-  {
-    patientsTemplates_[ResourceType_Patient] = "{{PatientID}} - {{PatientName}}";
-    patientsTemplates_[ResourceType_Study] = "{{StudyDate}} - {{StudyDescription}}";
-    patientsTemplates_[ResourceType_Series] = "{{Modality}} - {{SeriesDescription}}";
-
-    studiesTemplates_[ResourceType_Study] = "{{PatientID}} - {{PatientName}} - {{StudyDescription}}";
-    studiesTemplates_[ResourceType_Series] = patientsTemplates_[ResourceType_Series];
-
-    patients_.reset(new RootNode(context, ResourceType_Patient, patientsTemplates_));
-    studies_.reset(new RootNode(context, ResourceType_Study, studiesTemplates_));
-    dates_.reset(new ListOfStudiesByYear(context, studiesTemplates_));
-  }
-
-  virtual ~DummyBucket2()
-  {
-    Stop();
-  }
-
-  virtual bool IsExistingFolder(const UriComponents& path) ORTHANC_OVERRIDE
-  {
-    if (path.empty())
-    {
-      return true;
-    }
-    else if (path[0] == BY_UIDS)
-    {
-      return (path.size() <= 3 &&
-              (path.size() != 3 || path[2] != "study.json"));
-    }
-    else if (path[0] == BY_PATIENTS)
-    {
-      IWebDavBucket::Collection tmp;
-      return patients_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (path[0] == BY_STUDIES)
-    {
-      IWebDavBucket::Collection tmp;
-      return studies_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (path[0] == BY_DATE)
-    {
-      IWebDavBucket::Collection tmp;
-      return dates_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (path[0] == UPLOADS)
-    {
-      return uploads_.IsExistingFolder(UriComponents(path.begin() + 1, path.end()));
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  virtual bool ListCollection(Collection& collection,
-                              const UriComponents& path) ORTHANC_OVERRIDE
-  {
-    if (path.empty())
-    {
-      collection.AddResource(new Folder(BY_DATE));
-      collection.AddResource(new Folder(BY_PATIENTS));
-      collection.AddResource(new Folder(BY_STUDIES));
-      collection.AddResource(new Folder(BY_UIDS));
-      collection.AddResource(new Folder(UPLOADS));
-      return true;
-    }   
-    else if (path[0] == BY_UIDS)
-    {
-      DatabaseLookup query;
-      ResourceType level;
-      size_t limit = 0;  // By default, no limits
-
-      if (path.size() == 1)
-      {
-        level = ResourceType_Study;
-        limit = 100;  // TODO
-      }
-      else if (path.size() == 2)
-      {
-        AddVirtualFile(collection, path, "study.json");
-
-        level = ResourceType_Series;
-        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
-                                true /* case sensitive */, true /* mandatory tag */);
-      }      
-      else if (path.size() == 3)
-      {
-        AddVirtualFile(collection, path, "series.json");
-
-        level = ResourceType_Instance;
-        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
-                                true /* case sensitive */, true /* mandatory tag */);
-        query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
-                                true /* case sensitive */, true /* mandatory tag */);
-      }
-      else
-      {
-        return false;
-      }
-
-      DicomIdentifiersVisitor visitor(context_, collection, level);
-      context_.Apply(visitor, query, level, 0 /* since */, limit);
-      
-      return true;
-    }
-    else if (path[0] == BY_PATIENTS)
-    {
-      return patients_->ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (path[0] == BY_STUDIES)
-    {
-      return studies_->ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (path[0] == BY_DATE)
-    {
-      return dates_->ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (path[0] == UPLOADS)
-    {
-      return uploads_.ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  virtual bool GetFileContent(MimeType& mime,
-                              std::string& content,
-                              boost::posix_time::ptime& modificationTime, 
-                              const UriComponents& path) ORTHANC_OVERRIDE
-  {
-    if (path.empty())
-    {
-      return false;
-    }
-    else if (path[0] == BY_UIDS)
-    {
-      if (path.size() == 3 &&
-          path[2] == "study.json")
-      {
-        DatabaseLookup query;
-        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
-                                true /* case sensitive */, true /* mandatory tag */);
-      
-        OrthancJsonVisitor visitor(context_, content, ResourceType_Study);
-        context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
-
-        mime = MimeType_Json;
-        return visitor.IsSuccess();
-      }
-      else if (path.size() == 4 &&
-               path[3] == "series.json")
-      {
-        DatabaseLookup query;
-        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
-                                true /* case sensitive */, true /* mandatory tag */);
-        query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
-                                true /* case sensitive */, true /* mandatory tag */);
-      
-        OrthancJsonVisitor visitor(context_, content, ResourceType_Series);
-        context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */);
-
-        mime = MimeType_Json;
-        return visitor.IsSuccess();
-      }
-      else if (path.size() == 4 &&
-               boost::ends_with(path[3], ".dcm"))
-      {
-        std::string sopInstanceUid = path[3];
-        sopInstanceUid.resize(sopInstanceUid.size() - 4);
-        
-        DatabaseLookup query;
-        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
-                                true /* case sensitive */, true /* mandatory tag */);
-        query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
-                                true /* case sensitive */, true /* mandatory tag */);
-        query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid,
-                                true /* case sensitive */, true /* mandatory tag */);
-      
-        DicomFileVisitor visitor(context_, content, modificationTime);
-        context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */);
-        
-        mime = MimeType_Dicom;
-        return visitor.IsSuccess();
-      }
-      else
-      {
-        return false;
-      }
-    }
-    else if (path[0] == BY_PATIENTS)
-    {
-      return patients_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (path[0] == BY_STUDIES)
-    {
-      return studies_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
-    }
-    else if (path[0] == UPLOADS)
-    {
-      return uploads_.GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  
-  virtual bool StoreFile(const std::string& content,
-                         const UriComponents& path) ORTHANC_OVERRIDE
-  {
-    if (path.size() >= 1 &&
-        path[0] == UPLOADS)
-    {
-      UriComponents subpath(UriComponents(path.begin() + 1, path.end()));
-
-      if (uploads_.StoreFile(content, subpath))
-      {
-        if (!content.empty())
-        {
-          uploadQueue_.Enqueue(new SingleValueObject<std::string>(Toolbox::FlattenUri(subpath)));
-        }
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  virtual bool CreateFolder(const UriComponents& path)
-  {
-    if (path.size() >= 1 &&
-        path[0] == UPLOADS)
-    {
-      return uploads_.CreateFolder(UriComponents(path.begin() + 1, path.end()));
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  virtual bool DeleteItem(const std::vector<std::string>& path) ORTHANC_OVERRIDE
-  {
-    if (path.size() >= 1 &&
-        path[0] == UPLOADS)
-    {
-      return uploads_.DeleteItem(UriComponents(path.begin() + 1, path.end()));
-    }
-    else
-    {
-      return false;  // read-only
-    }
-  }
-
-  virtual void Start() ORTHANC_OVERRIDE
-  {
-    if (running_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      LOG(INFO) << "Starting the WebDAV upload thread";
-      running_ = true;
-      uploadThread_ = boost::thread(UploadWorker, this);
-    }
-  }
-
-  virtual void Stop() ORTHANC_OVERRIDE
-  {
-    if (running_)
-    {
-      LOG(INFO) << "Stopping the WebDAV upload thread";
-      running_ = false;
-      if (uploadThread_.joinable())
-      {
-        uploadThread_.join();
-      }
-    }
-  }
-};
-
-
-
 static void PrintHelp(const char* path)
 {
   std::cout 
@@ -2653,9 +1053,7 @@
       UriComponents root;  // TODO
       root.push_back("a");
       root.push_back("b");
-      //httpServer.Register(root, new WebDavStorage(true));
-      //httpServer.Register(root, new DummyBucket(context, true));
-      httpServer.Register(root, new DummyBucket2(context));
+      httpServer.Register(root, new OrthancWebDav(context));
     }
 
     if (httpServer.GetPortNumber() < 1024)