diff OrthancServer/Sources/OrthancWebDav.cpp @ 4240:799c0c527ced

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 09 Oct 2020 12:02:40 +0200
parents
children 3510da0e260c
line wrap: on
line diff
--- /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();
+      }
+    }
+  }
+}