view OrthancServer/Sources/OrthancWebDav.cpp @ 5911:bfae0fc2ea1b get-scu-test

Started to work on handling errors as warnings when trying to store instances whose SOPClassUID has not been accepted during the negotiation. Work to be finalized later
author Alain Mazy <am@orthanc.team>
date Mon, 09 Dec 2024 10:07:19 +0100
parents f7adfb22e20e
children 1fab9ddaf702
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include "OrthancWebDav.h"

#include "../../OrthancFramework/Sources/Compression/ZipReader.h"
#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
#include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h"
#include "../../OrthancFramework/Sources/Logging.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_DATES = "by-dates";
static const char* const BY_UIDS = "by-uids";
static const char* const UPLOADS = "uploads";
static const char* const STUDY_INFO = "study.json";
static const char* const SERIES_INFO = "series.json";


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,
                         ResourceType level,
                         MetadataType metadata)
  {
    std::string value;
    int64_t revision;  // Ignored
    if (context.GetIndex().LookupMetadata(value, revision, publicId, level, 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;
          int64_t revision;  // Ignored
          if (context_.GetIndex().LookupAttachment(info, revision, 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, level_, 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, ResourceType_Instance, 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 resource;
      std::set<DicomTag> emptyRequestedTags;  // not supported for webdav

      if (context_.ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human, emptyRequestedTags, true /* allowStorageAccess */))
      {
        if (success_)
        {
          success_ = false;  // Two matches => Error
        }
        else
        {
          target_ = resource.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_;

    static bool LookupInstanceId(std::string& instanceId,
                                 const UriComponents& path)
    {
      if (path.size() == 1 &&
          boost::ends_with(path[0], ".dcm"))
      {
        instanceId = path[0].substr(0, path[0].size() - 4);
        return true;
      }
      else
      {
        return false;
      }
    }

  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, ResourceType_Instance, MetadataType_Instance_ReceptionDate);

          FileInfo info;
          int64_t revision;  // Ignored
          if (context_.GetIndex().LookupAttachment(info, revision, *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
    {
      std::string instanceId;
      if (LookupInstanceId(instanceId, path))
      {
        try
        {
          mime = MimeType_Dicom;
          context_.ReadDicom(content, instanceId);
          LookupTime(time, context_, instanceId, ResourceType_Instance, MetadataType_Instance_ReceptionDate);
          return true;
        }
        catch (OrthancException&)
        {
          // File was removed
          return false;
        }
      }
      else
      {
        return false;
      }
    }

    virtual bool DeleteItem(const UriComponents& path) ORTHANC_OVERRIDE
    {
      if (path.empty())
      {
        // Delete all
        std::list<std::string> resources;
        try
        {
          context_.GetIndex().GetChildren(resources, parentSeries_);
        }
        catch (OrthancException&)
        {
          // Unknown (or deleted) parent series
          return true;
        }

        for (std::list<std::string>::const_iterator it = resources.begin();
             it != resources.end(); ++it)
        {
          Json::Value info;
          context_.DeleteResource(info, *it, ResourceType_Instance);
        }

        return true;
      }
      else
      {
        std::string instanceId;
        if (LookupInstanceId(instanceId, path))
        {
          Json::Value info;
          return context_.DeleteResource(info, instanceId, ResourceType_Instance);
        }
        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* node = CreateSubfolder(path);
        
        if (node == NULL)
        {
          return NULL;
        }
        else
        {
          children_[path] = node;
          return node;
        }
      }
      else
      {
        assert(child->second != NULL);
        return child->second;
      }
    }

  protected:
    void InvalidateSubfolder(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* CreateSubfolder(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)
        {
          // Must be "true" to allow DELETE on folders that are
          // automatically removed through recursive deletion
          return true;
        }
        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);
        }
      }
    }


    virtual bool DeleteItem(const UriComponents& path) ORTHANC_OVERRIDE ORTHANC_FINAL
    {
      Refresh();

      if (path.empty())
      {
        IWebDavBucket::Collection collection;
        if (ListSubfolders(collection))
        {
          std::set<std::string> content;
          collection.ListDisplayNames(content);

          for (std::set<std::string>::const_iterator
                 it = content.begin(); it != content.end(); ++it)
          {
            INode* node = GetChild(*it);
            if (node)
            {
              node->DeleteItem(path);
            }
          }

          return true;
        }
        else
        {
          return false;
        }
      }
      else
      {
        INode* child = GetChild(path[0]);
        if (child == NULL)
        {
          return true;
        }
        else
        {
          // Recursivity
          UriComponents subpath(path.begin() + 1, path.end());
          return child->DeleteItem(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)
      {
        InvalidateSubfolder(*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, index_->GetLevel(), timeMetadata_);

          std::unique_ptr<IWebDavBucket::Resource> resource(new IWebDavBucket::Folder(it->first));
          resource->SetCreationTime(time);
          target.AddResource(resource.release());
        }

        return true;
      }
    }

    virtual INode* CreateSubfolder(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:
      explicit 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:
      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* CreateSubfolder(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* CreateSubfolder(const std::string& path) ORTHANC_OVERRIDE
    {
      return new ListOfStudiesByMonth(context_, path, templates_);
    }

  public:
    ListOfStudiesByYear(ServerContext& context,
                        const Templates& templates) :
      context_(context),
      templates_(templates)
    {
    }
  };


  class OrthancWebDav::DicomDeleteVisitor : public ServerContext::ILookupVisitor
  {
  private:
    ServerContext&  context_;
    ResourceType    level_;

  public:
    DicomDeleteVisitor(ServerContext& context,
                       ResourceType level) :
      context_(context),
      level_(level)
    {
    }

    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   /* unused     */,
                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
    {
      Json::Value info;
      context_.DeleteResource(info, publicId, level_);
    }
  };
  

  void OrthancWebDav::AddVirtualFile(Collection& collection,
                                     const UriComponents& path,
                                     const std::string& filename)
  {
    MimeType mime;
    std::string content;
    boost::posix_time::ptime modification;  // Unused, let the date be set to "GetNow()"

    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());
      collection.AddResource(f.release());
    }
  }


  void OrthancWebDav::UploadWorker(OrthancWebDav* that)
  {
    Logging::SetCurrentThreadName("WEBDAV-UPLOAD");

    assert(that != NULL);

    boost::posix_time::ptime lastModification = GetNow();

    while (that->uploadRunning_)
    {
      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(30))
      {
        /**
         * After every 30 seconds of inactivity, remove the empty
         * folders. This delay is needed to avoid removing
         * just-created folders before the remote WebDAV has time to
         * write files into it.
         **/
        LOG(TRACE) << "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))
    {
      bool success = false;

      if (ZipReader::IsZipMemoryBuffer(content))
      {
        // New in Orthanc 1.8.2
        std::unique_ptr<ZipReader> reader(ZipReader::CreateFromMemory(content));

        std::string filename, uncompressedFile;
        while (reader->ReadNextFile(filename, uncompressedFile))
        {
          if (!uncompressedFile.empty())
          {
            LOG(INFO) << "Uploading DICOM file extracted from a ZIP archive in WebDAV: " << filename;
          
            std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(uncompressedFile));
            instance->SetOrigin(DicomInstanceOrigin::FromWebDav());

            std::string publicId;

            try
            {
              context_.Store(publicId, *instance, StoreInstanceMode_Default);
            }
            catch (OrthancException& e)
            {
              if (e.GetErrorCode() == ErrorCode_BadFileFormat)
              {
                LOG(ERROR) << "Cannot import non-DICOM file from ZIP archive: " << filename;
              }
            }
          }
        }

        success = true;
      }
      else
      {
        std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(content));
        instance->SetOrigin(DicomInstanceOrigin::FromWebDav());

        try
        {
          std::string publicId;
          ServerContext::StoreResult result = context_.Store(publicId, *instance, StoreInstanceMode_Default);
          if (result.GetStatus() == StoreStatus_Success ||
              result.GetStatus() == StoreStatus_AlreadyStored)
          {
            LOG(INFO) << "Successfully imported DICOM instance from WebDAV: "
                      << path << " (Orthanc ID: " << publicId << ")";
            success = true;
          }
        }
        catch (OrthancException& e)
        {
        }
      }

      uploads_.DeleteItem(uri);

      if (!success)
      {
        LOG(WARNING) << "Cannot import DICOM instance from WebWAV (maybe not a DICOM file): " << path;
      }
    }
  }


  OrthancWebDav::INode& OrthancWebDav::GetRootNode(const std::string& rootPath)
  {
    if (rootPath == BY_PATIENTS)
    {
      return *patients_;
    }
    else if (rootPath == BY_STUDIES)
    {
      return *studies_;
    }
    else if (rootPath == BY_DATES)
    {
      return *dates_;
    }
    else
    {
      throw OrthancException(ErrorCode_InternalError);
    }
  }
  

  OrthancWebDav::OrthancWebDav(ServerContext& context,
                               bool allowDicomDelete,
                               bool allowUpload) :
    context_(context),
    allowDicomDelete_(allowDicomDelete),
    allowUpload_(allowUpload),
    uploads_(false /* store uploads as temporary files */),
    uploadRunning_(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_INFO));
    }
    else if (path[0] == BY_PATIENTS ||
             path[0] == BY_STUDIES ||
             path[0] == BY_DATES)
    {
      IWebDavBucket::Collection collection;
      return GetRootNode(path[0]).ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
    }
    else if (allowUpload_ &&
             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_DATES));
      collection.AddResource(new Folder(BY_PATIENTS));
      collection.AddResource(new Folder(BY_STUDIES));
      collection.AddResource(new Folder(BY_UIDS));

      if (allowUpload_)
      {
        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 = 0;  // TODO - Should we limit here?
      }
      else if (path.size() == 2)
      {
        AddVirtualFile(collection, path, STUDY_INFO);

        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_INFO);

        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 ||
             path[0] == BY_STUDIES ||
             path[0] == BY_DATES)
    {
      return GetRootNode(path[0]).ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
    }
    else if (allowUpload_ &&
             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_INFO)
      {
        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_INFO)
      {
        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"))
      {
        const std::string sopInstanceUid = path[3].substr(0, path[3].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 ||
             path[0] == BY_STUDIES ||
             path[0] == BY_DATES)
    {
      return GetRootNode(path[0]).GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
    }
    else if (allowUpload_ &&
             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 (allowUpload_ &&
        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 (allowUpload_ &&
        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.empty())
    {
      return false;
    }
    else if (path[0] == BY_UIDS &&
             path.size() >= 2 &&
             path.size() <= 4)
    {
      if (allowDicomDelete_)
      {
        ResourceType level;
        DatabaseLookup query;

        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
                                true /* case sensitive */, true /* mandatory tag */);
        level = ResourceType_Study;

        if (path.size() >= 3)
        {
          if (path[2] == STUDY_INFO)
          {
            return true;  // Allow deletion of virtual files (to avoid blocking recursive DELETE)
          }
          
          query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
                                  true /* case sensitive */, true /* mandatory tag */);
          level = ResourceType_Series;
        }

        if (path.size() == 4)
        {
          if (path[3] == SERIES_INFO)
          {
            return true;  // Allow deletion of virtual files (to avoid blocking recursive DELETE)
          }
          else if (boost::ends_with(path[3], ".dcm"))
          {
            const std::string sopInstanceUid = path[3].substr(0, path[3].size() - 4);

            query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid,
                                    true /* case sensitive */, true /* mandatory tag */);
            level = ResourceType_Instance;
          }
          else
          {
            return false;
          }
        }

        DicomDeleteVisitor visitor(context_, level);
        context_.Apply(visitor, query, level, 0 /* since */, 0 /* no limit */);
        return true;
      }
      else
      {
        return false;  // read-only
      }
    }
    else if (path[0] == BY_PATIENTS ||
             path[0] == BY_STUDIES ||
             path[0] == BY_DATES)
    {
      if (allowDicomDelete_)
      {
        return GetRootNode(path[0]).DeleteItem(UriComponents(path.begin() + 1, path.end()));
      }
      else
      {
        return false;  // read-only
      }
    }
    else if (allowUpload_ &&
             path[0] == UPLOADS)
    {
      return uploads_.DeleteItem(UriComponents(path.begin() + 1, path.end()));
    }
    else
    {
      return false;
    }
  }

  
  void OrthancWebDav::Start() 
  {
    if (uploadRunning_)
    {
      throw OrthancException(ErrorCode_BadSequenceOfCalls);
    }
    else if (allowUpload_)
    {
      LOG(INFO) << "Starting the WebDAV upload thread";
      uploadRunning_ = true;
      uploadThread_ = boost::thread(UploadWorker, this);
    }
  }

  
  void OrthancWebDav::Stop() 
  {
    if (uploadRunning_)
    {
      LOG(INFO) << "Stopping the WebDAV upload thread";
      uploadRunning_ = false;
      if (uploadThread_.joinable())
      {
        uploadThread_.join();
      }
    }
  }
}