comparison 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
comparison
equal deleted inserted replaced
4239:c8754c4c1862 4240:799c0c527ced
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2020 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * In addition, as a special exception, the copyright holders of this
13 * program give permission to link the code of its release with the
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it
15 * that use the same license as the "OpenSSL" library), and distribute
16 * the linked executables. You must obey the GNU General Public License
17 * in all respects for all of the code used other than "OpenSSL". If you
18 * modify file(s) with this exception, you may extend this exception to
19 * your version of the file(s), but you are not obligated to do so. If
20 * you do not wish to do so, delete this exception statement from your
21 * version. If you delete this exception statement from all source files
22 * in the program, then also delete it here.
23 *
24 * This program is distributed in the hope that it will be useful, but
25 * WITHOUT ANY WARRANTY; without even the implied warranty of
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27 * General Public License for more details.
28 *
29 * You should have received a copy of the GNU General Public License
30 * along with this program. If not, see <http://www.gnu.org/licenses/>.
31 **/
32
33
34 #include "OrthancWebDav.h"
35
36 #include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
37 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
38 #include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h"
39 #include "Search/DatabaseLookup.h"
40 #include "ServerContext.h"
41
42 #include <boost/regex.hpp>
43 #include <boost/algorithm/string/predicate.hpp>
44
45
46 static const char* const BY_PATIENTS = "by-patients";
47 static const char* const BY_STUDIES = "by-studies";
48 static const char* const BY_DATE = "by-dates";
49 static const char* const BY_UIDS = "by-uids";
50 static const char* const UPLOADS = "uploads";
51 static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
52
53
54 namespace Orthanc
55 {
56 static boost::posix_time::ptime GetNow()
57 {
58 return boost::posix_time::second_clock::universal_time();
59 }
60
61
62 static void LookupTime(boost::posix_time::ptime& target,
63 ServerContext& context,
64 const std::string& publicId,
65 MetadataType metadata)
66 {
67 std::string value;
68 if (context.GetIndex().LookupMetadata(value, publicId, metadata))
69 {
70 try
71 {
72 target = boost::posix_time::from_iso_string(value);
73 return;
74 }
75 catch (std::exception& e)
76 {
77 }
78 }
79
80 target = GetNow();
81 }
82
83
84 class OrthancWebDav::DicomIdentifiersVisitor : public ServerContext::ILookupVisitor
85 {
86 private:
87 ServerContext& context_;
88 bool isComplete_;
89 Collection& target_;
90 ResourceType level_;
91
92 public:
93 DicomIdentifiersVisitor(ServerContext& context,
94 Collection& target,
95 ResourceType level) :
96 context_(context),
97 isComplete_(false),
98 target_(target),
99 level_(level)
100 {
101 }
102
103 virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
104 {
105 return false; // (*)
106 }
107
108 virtual void MarkAsComplete() ORTHANC_OVERRIDE
109 {
110 isComplete_ = true; // TODO
111 }
112
113 virtual void Visit(const std::string& publicId,
114 const std::string& instanceId /* unused */,
115 const DicomMap& mainDicomTags,
116 const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE
117 {
118 DicomTag tag(0, 0);
119 MetadataType timeMetadata;
120
121 switch (level_)
122 {
123 case ResourceType_Study:
124 tag = DICOM_TAG_STUDY_INSTANCE_UID;
125 timeMetadata = MetadataType_LastUpdate;
126 break;
127
128 case ResourceType_Series:
129 tag = DICOM_TAG_SERIES_INSTANCE_UID;
130 timeMetadata = MetadataType_LastUpdate;
131 break;
132
133 case ResourceType_Instance:
134 tag = DICOM_TAG_SOP_INSTANCE_UID;
135 timeMetadata = MetadataType_Instance_ReceptionDate;
136 break;
137
138 default:
139 throw OrthancException(ErrorCode_InternalError);
140 }
141
142 std::string s;
143 if (mainDicomTags.LookupStringValue(s, tag, false) &&
144 !s.empty())
145 {
146 std::unique_ptr<Resource> resource;
147
148 if (level_ == ResourceType_Instance)
149 {
150 FileInfo info;
151 if (context_.GetIndex().LookupAttachment(info, publicId, FileContentType_Dicom))
152 {
153 std::unique_ptr<File> f(new File(s + ".dcm"));
154 f->SetMimeType(MimeType_Dicom);
155 f->SetContentLength(info.GetUncompressedSize());
156 resource.reset(f.release());
157 }
158 }
159 else
160 {
161 resource.reset(new Folder(s));
162 }
163
164 if (resource.get() != NULL)
165 {
166 boost::posix_time::ptime t;
167 LookupTime(t, context_, publicId, timeMetadata);
168 resource->SetCreationTime(t);
169 target_.AddResource(resource.release());
170 }
171 }
172 }
173 };
174
175
176 class OrthancWebDav::DicomFileVisitor : public ServerContext::ILookupVisitor
177 {
178 private:
179 ServerContext& context_;
180 bool success_;
181 std::string& target_;
182 boost::posix_time::ptime& time_;
183
184 public:
185 DicomFileVisitor(ServerContext& context,
186 std::string& target,
187 boost::posix_time::ptime& time) :
188 context_(context),
189 success_(false),
190 target_(target),
191 time_(time)
192 {
193 }
194
195 bool IsSuccess() const
196 {
197 return success_;
198 }
199
200 virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
201 {
202 return false; // (*)
203 }
204
205 virtual void MarkAsComplete() ORTHANC_OVERRIDE
206 {
207 }
208
209 virtual void Visit(const std::string& publicId,
210 const std::string& instanceId /* unused */,
211 const DicomMap& mainDicomTags,
212 const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE
213 {
214 if (success_)
215 {
216 success_ = false; // Two matches => Error
217 }
218 else
219 {
220 LookupTime(time_, context_, publicId, MetadataType_Instance_ReceptionDate);
221 context_.ReadDicom(target_, publicId);
222 success_ = true;
223 }
224 }
225 };
226
227
228 class OrthancWebDav::OrthancJsonVisitor : public ServerContext::ILookupVisitor
229 {
230 private:
231 ServerContext& context_;
232 bool success_;
233 std::string& target_;
234 ResourceType level_;
235
236 public:
237 OrthancJsonVisitor(ServerContext& context,
238 std::string& target,
239 ResourceType level) :
240 context_(context),
241 success_(false),
242 target_(target),
243 level_(level)
244 {
245 }
246
247 bool IsSuccess() const
248 {
249 return success_;
250 }
251
252 virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
253 {
254 return false; // (*)
255 }
256
257 virtual void MarkAsComplete() ORTHANC_OVERRIDE
258 {
259 }
260
261 virtual void Visit(const std::string& publicId,
262 const std::string& instanceId /* unused */,
263 const DicomMap& mainDicomTags,
264 const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE
265 {
266 Json::Value info;
267 if (context_.GetIndex().LookupResource(info, publicId, level_))
268 {
269 if (success_)
270 {
271 success_ = false; // Two matches => Error
272 }
273 else
274 {
275 target_ = info.toStyledString();
276
277 // Replace UNIX newlines with DOS newlines
278 boost::replace_all(target_, "\n", "\r\n");
279
280 success_ = true;
281 }
282 }
283 }
284 };
285
286
287 class OrthancWebDav::ResourcesIndex : public boost::noncopyable
288 {
289 public:
290 typedef std::map<std::string, std::string> Map;
291
292 private:
293 ServerContext& context_;
294 ResourceType level_;
295 std::string template_;
296 Map pathToResource_;
297 Map resourceToPath_;
298
299 void CheckInvariants()
300 {
301 #ifndef NDEBUG
302 assert(pathToResource_.size() == resourceToPath_.size());
303
304 for (Map::const_iterator it = pathToResource_.begin(); it != pathToResource_.end(); ++it)
305 {
306 assert(resourceToPath_[it->second] == it->first);
307 }
308
309 for (Map::const_iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it)
310 {
311 assert(pathToResource_[it->second] == it->first);
312 }
313 #endif
314 }
315
316 void AddTags(DicomMap& target,
317 const std::string& resourceId,
318 ResourceType tagsFromLevel)
319 {
320 DicomMap tags;
321 if (context_.GetIndex().GetMainDicomTags(tags, resourceId, level_, tagsFromLevel))
322 {
323 target.Merge(tags);
324 }
325 }
326
327 void Register(const std::string& resourceId)
328 {
329 // Don't register twice the same resource
330 if (resourceToPath_.find(resourceId) == resourceToPath_.end())
331 {
332 std::string name = template_;
333
334 DicomMap tags;
335
336 AddTags(tags, resourceId, level_);
337
338 if (level_ == ResourceType_Study)
339 {
340 AddTags(tags, resourceId, ResourceType_Patient);
341 }
342
343 DicomArray arr(tags);
344 for (size_t i = 0; i < arr.GetSize(); i++)
345 {
346 const DicomElement& element = arr.GetElement(i);
347 if (!element.GetValue().IsNull() &&
348 !element.GetValue().IsBinary())
349 {
350 const std::string tag = FromDcmtkBridge::GetTagName(element.GetTag(), "");
351 boost::replace_all(name, "{{" + tag + "}}", element.GetValue().GetContent());
352 }
353 }
354
355 // Blank the tags that were not matched
356 static const boost::regex REGEX_BLANK_TAGS("{{.*?}}"); // non-greedy match
357 name = boost::regex_replace(name, REGEX_BLANK_TAGS, "");
358
359 // UTF-8 characters cannot be used on Windows XP
360 name = Toolbox::ConvertToAscii(name);
361 boost::replace_all(name, "/", "");
362 boost::replace_all(name, "\\", "");
363
364 // Trim sequences of spaces as one single space
365 static const boost::regex REGEX_TRIM_SPACES("{{.*?}}");
366 name = boost::regex_replace(name, REGEX_TRIM_SPACES, " ");
367 name = Toolbox::StripSpaces(name);
368
369 size_t count = 0;
370 for (;;)
371 {
372 std::string path = name;
373 if (count > 0)
374 {
375 path += " (" + boost::lexical_cast<std::string>(count) + ")";
376 }
377
378 if (pathToResource_.find(path) == pathToResource_.end())
379 {
380 pathToResource_[path] = resourceId;
381 resourceToPath_[resourceId] = path;
382 return;
383 }
384
385 count++;
386 }
387
388 throw OrthancException(ErrorCode_InternalError);
389 }
390 }
391
392 public:
393 ResourcesIndex(ServerContext& context,
394 ResourceType level,
395 const std::string& templateString) :
396 context_(context),
397 level_(level),
398 template_(templateString)
399 {
400 }
401
402 ResourceType GetLevel() const
403 {
404 return level_;
405 }
406
407 void Refresh(std::set<std::string>& removedPaths /* out */,
408 const std::set<std::string>& resources)
409 {
410 CheckInvariants();
411
412 // Detect the resources that have been removed since last refresh
413 removedPaths.clear();
414 std::set<std::string> removedResources;
415
416 for (Map::iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it)
417 {
418 if (resources.find(it->first) == resources.end())
419 {
420 const std::string& path = it->second;
421
422 assert(pathToResource_.find(path) != pathToResource_.end());
423 pathToResource_.erase(path);
424 removedPaths.insert(path);
425
426 removedResources.insert(it->first); // Delay the removal to avoid disturbing the iterator
427 }
428 }
429
430 // Remove the missing resources
431 for (std::set<std::string>::const_iterator it = removedResources.begin(); it != removedResources.end(); ++it)
432 {
433 assert(resourceToPath_.find(*it) != resourceToPath_.end());
434 resourceToPath_.erase(*it);
435 }
436
437 CheckInvariants();
438
439 for (std::set<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it)
440 {
441 Register(*it);
442 }
443
444 CheckInvariants();
445 }
446
447 const Map& GetPathToResource() const
448 {
449 return pathToResource_;
450 }
451 };
452
453
454 class OrthancWebDav::InstancesOfSeries : public INode
455 {
456 private:
457 ServerContext& context_;
458 std::string parentSeries_;
459
460 public:
461 InstancesOfSeries(ServerContext& context,
462 const std::string& parentSeries) :
463 context_(context),
464 parentSeries_(parentSeries)
465 {
466 }
467
468 virtual bool ListCollection(IWebDavBucket::Collection& target,
469 const UriComponents& path) ORTHANC_OVERRIDE
470 {
471 if (path.empty())
472 {
473 std::list<std::string> resources;
474 try
475 {
476 context_.GetIndex().GetChildren(resources, parentSeries_);
477 }
478 catch (OrthancException&)
479 {
480 // Unknown (or deleted) parent series
481 return false;
482 }
483
484 for (std::list<std::string>::const_iterator
485 it = resources.begin(); it != resources.end(); ++it)
486 {
487 boost::posix_time::ptime time;
488 LookupTime(time, context_, *it, MetadataType_Instance_ReceptionDate);
489
490 FileInfo info;
491 if (context_.GetIndex().LookupAttachment(info, *it, FileContentType_Dicom))
492 {
493 std::unique_ptr<File> resource(new File(*it + ".dcm"));
494 resource->SetMimeType(MimeType_Dicom);
495 resource->SetContentLength(info.GetUncompressedSize());
496 resource->SetCreationTime(time);
497 target.AddResource(resource.release());
498 }
499 }
500
501 return true;
502 }
503 else
504 {
505 return false;
506 }
507 }
508
509 virtual bool GetFileContent(MimeType& mime,
510 std::string& content,
511 boost::posix_time::ptime& time,
512 const UriComponents& path) ORTHANC_OVERRIDE
513 {
514 if (path.size() == 1 &&
515 boost::ends_with(path[0], ".dcm"))
516 {
517 std::string instanceId = path[0].substr(0, path[0].size() - 4);
518
519 try
520 {
521 mime = MimeType_Dicom;
522 context_.ReadDicom(content, instanceId);
523 LookupTime(time, context_, instanceId, MetadataType_Instance_ReceptionDate);
524 return true;
525 }
526 catch (OrthancException&)
527 {
528 // File was removed
529 return false;
530 }
531 }
532 else
533 {
534 return false;
535 }
536 }
537 };
538
539
540
541 /**
542 * The "InternalNode" class corresponds to a non-leaf node in the
543 * WebDAV tree, that only contains subfolders (no file).
544 *
545 * TODO: Implement a LRU index to dynamically remove the oldest
546 * children on high RAM usage.
547 **/
548 class OrthancWebDav::InternalNode : public INode
549 {
550 private:
551 typedef std::map<std::string, INode*> Children;
552
553 Children children_;
554
555 INode* GetChild(const std::string& path) // Don't delete the result pointer!
556 {
557 Children::const_iterator child = children_.find(path);
558 if (child == children_.end())
559 {
560 INode* child = CreateChild(path);
561
562 if (child == NULL)
563 {
564 return NULL;
565 }
566 else
567 {
568 children_[path] = child;
569 return child;
570 }
571 }
572 else
573 {
574 assert(child->second != NULL);
575 return child->second;
576 }
577 }
578
579 protected:
580 void RemoveSubfolder(const std::string& path)
581 {
582 Children::iterator child = children_.find(path);
583 if (child != children_.end())
584 {
585 assert(child->second != NULL);
586 delete child->second;
587 children_.erase(child);
588 }
589 }
590
591 virtual void Refresh() = 0;
592
593 virtual bool ListSubfolders(IWebDavBucket::Collection& target) = 0;
594
595 virtual INode* CreateChild(const std::string& path) = 0;
596
597 public:
598 virtual ~InternalNode()
599 {
600 for (Children::iterator it = children_.begin(); it != children_.end(); ++it)
601 {
602 assert(it->second != NULL);
603 delete it->second;
604 }
605 }
606
607 virtual bool ListCollection(IWebDavBucket::Collection& target,
608 const UriComponents& path)
609 ORTHANC_OVERRIDE ORTHANC_FINAL
610 {
611 Refresh();
612
613 if (path.empty())
614 {
615 return ListSubfolders(target);
616 }
617 else
618 {
619 // Recursivity
620 INode* child = GetChild(path[0]);
621 if (child == NULL)
622 {
623 return false;
624 }
625 else
626 {
627 UriComponents subpath(path.begin() + 1, path.end());
628 return child->ListCollection(target, subpath);
629 }
630 }
631 }
632
633 virtual bool GetFileContent(MimeType& mime,
634 std::string& content,
635 boost::posix_time::ptime& time,
636 const UriComponents& path)
637 ORTHANC_OVERRIDE ORTHANC_FINAL
638 {
639 if (path.empty())
640 {
641 return false; // An internal node doesn't correspond to a file
642 }
643 else
644 {
645 // Recursivity
646 Refresh();
647
648 INode* child = GetChild(path[0]);
649 if (child == NULL)
650 {
651 return false;
652 }
653 else
654 {
655 UriComponents subpath(path.begin() + 1, path.end());
656 return child->GetFileContent(mime, content, time, subpath);
657 }
658 }
659 }
660 };
661
662
663 class OrthancWebDav::ListOfResources : public InternalNode
664 {
665 private:
666 ServerContext& context_;
667 const Templates& templates_;
668 std::unique_ptr<ResourcesIndex> index_;
669 MetadataType timeMetadata_;
670
671 protected:
672 virtual void Refresh() ORTHANC_OVERRIDE ORTHANC_FINAL
673 {
674 std::list<std::string> resources;
675 GetCurrentResources(resources);
676
677 std::set<std::string> removedPaths;
678 index_->Refresh(removedPaths, std::set<std::string>(resources.begin(), resources.end()));
679
680 // Remove the children whose associated resource doesn't exist anymore
681 for (std::set<std::string>::const_iterator
682 it = removedPaths.begin(); it != removedPaths.end(); ++it)
683 {
684 RemoveSubfolder(*it);
685 }
686 }
687
688 virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE ORTHANC_FINAL
689 {
690 if (index_->GetLevel() == ResourceType_Instance)
691 {
692 // Not a collection, no subfolders
693 return false;
694 }
695 else
696 {
697 const ResourcesIndex::Map& paths = index_->GetPathToResource();
698
699 for (ResourcesIndex::Map::const_iterator it = paths.begin(); it != paths.end(); ++it)
700 {
701 boost::posix_time::ptime time;
702 LookupTime(time, context_, it->second, timeMetadata_);
703
704 std::unique_ptr<IWebDavBucket::Resource> resource(new IWebDavBucket::Folder(it->first));
705 resource->SetCreationTime(time);
706 target.AddResource(resource.release());
707 }
708
709 return true;
710 }
711 }
712
713 virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE ORTHANC_FINAL
714 {
715 ResourcesIndex::Map::const_iterator resource = index_->GetPathToResource().find(path);
716 if (resource == index_->GetPathToResource().end())
717 {
718 return NULL;
719 }
720 else
721 {
722 return CreateResourceNode(resource->second);
723 }
724 }
725
726 ServerContext& GetContext() const
727 {
728 return context_;
729 }
730
731 virtual void GetCurrentResources(std::list<std::string>& resources) = 0;
732
733 virtual INode* CreateResourceNode(const std::string& resource) = 0;
734
735 public:
736 ListOfResources(ServerContext& context,
737 ResourceType level,
738 const Templates& templates) :
739 context_(context),
740 templates_(templates)
741 {
742 Templates::const_iterator t = templates.find(level);
743 if (t == templates.end())
744 {
745 throw OrthancException(ErrorCode_ParameterOutOfRange);
746 }
747
748 index_.reset(new ResourcesIndex(context, level, t->second));
749
750 if (level == ResourceType_Instance)
751 {
752 timeMetadata_ = MetadataType_Instance_ReceptionDate;
753 }
754 else
755 {
756 timeMetadata_ = MetadataType_LastUpdate;
757 }
758 }
759
760 ResourceType GetLevel() const
761 {
762 return index_->GetLevel();
763 }
764
765 const Templates& GetTemplates() const
766 {
767 return templates_;
768 }
769 };
770
771
772
773 class OrthancWebDav::SingleDicomResource : public ListOfResources
774 {
775 private:
776 std::string parentId_;
777
778 protected:
779 virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
780 {
781 try
782 {
783 GetContext().GetIndex().GetChildren(resources, parentId_);
784 }
785 catch (OrthancException&)
786 {
787 // Unknown parent resource
788 resources.clear();
789 }
790 }
791
792 virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
793 {
794 if (GetLevel() == ResourceType_Instance)
795 {
796 return NULL;
797 }
798 else if (GetLevel() == ResourceType_Series)
799 {
800 return new InstancesOfSeries(GetContext(), resource);
801 }
802 else
803 {
804 ResourceType l = GetChildResourceType(GetLevel());
805 return new SingleDicomResource(GetContext(), l, resource, GetTemplates());
806 }
807 }
808
809 public:
810 SingleDicomResource(ServerContext& context,
811 ResourceType level,
812 const std::string& parentId,
813 const Templates& templates) :
814 ListOfResources(context, level, templates),
815 parentId_(parentId)
816 {
817 }
818 };
819
820
821 class OrthancWebDav::RootNode : public ListOfResources
822 {
823 protected:
824 virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
825 {
826 GetContext().GetIndex().GetAllUuids(resources, GetLevel());
827 }
828
829 virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
830 {
831 if (GetLevel() == ResourceType_Series)
832 {
833 return new InstancesOfSeries(GetContext(), resource);
834 }
835 else
836 {
837 ResourceType l = GetChildResourceType(GetLevel());
838 return new SingleDicomResource(GetContext(), l, resource, GetTemplates());
839 }
840 }
841
842 public:
843 RootNode(ServerContext& context,
844 ResourceType level,
845 const Templates& templates) :
846 ListOfResources(context, level, templates)
847 {
848 }
849 };
850
851
852 class OrthancWebDav::ListOfStudiesByDate : public ListOfResources
853 {
854 private:
855 std::string year_;
856 std::string month_;
857
858 class Visitor : public ServerContext::ILookupVisitor
859 {
860 private:
861 std::list<std::string>& resources_;
862
863 public:
864 Visitor(std::list<std::string>& resources) :
865 resources_(resources)
866 {
867 }
868
869 virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
870 {
871 return false; // (*)
872 }
873
874 virtual void MarkAsComplete() ORTHANC_OVERRIDE
875 {
876 }
877
878 virtual void Visit(const std::string& publicId,
879 const std::string& instanceId /* unused */,
880 const DicomMap& mainDicomTags,
881 const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE
882 {
883 resources_.push_back(publicId);
884 }
885 };
886
887 protected:
888 virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
889 {
890 DatabaseLookup query;
891 query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + month_ + "01-" + year_ + month_ + "31",
892 true /* case sensitive */, true /* mandatory tag */);
893
894 Visitor visitor(resources);
895 GetContext().Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
896 }
897
898 virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
899 {
900 return new SingleDicomResource(GetContext(), ResourceType_Series, resource, GetTemplates());
901 }
902
903 public:
904 ListOfStudiesByDate(ServerContext& context,
905 const std::string& year,
906 const std::string& month,
907 const Templates& templates) :
908 ListOfResources(context, ResourceType_Study, templates),
909 year_(year),
910 month_(month)
911 {
912 if (year.size() != 4 ||
913 month.size() != 2)
914 {
915 throw OrthancException(ErrorCode_ParameterOutOfRange);
916 }
917 }
918 };
919
920
921 class OrthancWebDav::ListOfStudiesByMonth : public InternalNode
922 {
923 private:
924 ServerContext& context_;
925 std::string year_;
926 const Templates& templates_;
927
928 class Visitor : public ServerContext::ILookupVisitor
929 {
930 private:
931 std::set<std::string> months_;
932
933 public:
934 Visitor()
935 {
936 }
937
938 const std::set<std::string>& GetMonths() const
939 {
940 return months_;
941 }
942
943 virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
944 {
945 return false; // (*)
946 }
947
948 virtual void MarkAsComplete() ORTHANC_OVERRIDE
949 {
950 }
951
952 virtual void Visit(const std::string& publicId,
953 const std::string& instanceId /* unused */,
954 const DicomMap& mainDicomTags,
955 const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE
956 {
957 std::string s;
958 if (mainDicomTags.LookupStringValue(s, DICOM_TAG_STUDY_DATE, false) &&
959 s.size() == 8)
960 {
961 months_.insert(s.substr(4, 2)); // Get the month from "YYYYMMDD"
962 }
963 }
964 };
965
966 protected:
967 virtual void Refresh() ORTHANC_OVERRIDE
968 {
969 }
970
971 virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE
972 {
973 DatabaseLookup query;
974 query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + "0101-" + year_ + "1231",
975 true /* case sensitive */, true /* mandatory tag */);
976
977 Visitor visitor;
978 context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
979
980 for (std::set<std::string>::const_iterator it = visitor.GetMonths().begin();
981 it != visitor.GetMonths().end(); ++it)
982 {
983 target.AddResource(new IWebDavBucket::Folder(year_ + "-" + *it));
984 }
985
986 return true;
987 }
988
989 virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE
990 {
991 if (path.size() != 7) // Format: "YYYY-MM"
992 {
993 throw OrthancException(ErrorCode_InternalError);
994 }
995 else
996 {
997 const std::string year = path.substr(0, 4);
998 const std::string month = path.substr(5, 2);
999 return new ListOfStudiesByDate(context_, year, month, templates_);
1000 }
1001 }
1002
1003 public:
1004 ListOfStudiesByMonth(ServerContext& context,
1005 const std::string& year,
1006 const Templates& templates) :
1007 context_(context),
1008 year_(year),
1009 templates_(templates)
1010 {
1011 if (year_.size() != 4)
1012 {
1013 throw OrthancException(ErrorCode_ParameterOutOfRange);
1014 }
1015 }
1016 };
1017
1018
1019 class OrthancWebDav::ListOfStudiesByYear : public InternalNode
1020 {
1021 private:
1022 ServerContext& context_;
1023 const Templates& templates_;
1024
1025 protected:
1026 virtual void Refresh() ORTHANC_OVERRIDE
1027 {
1028 }
1029
1030 virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE
1031 {
1032 std::list<std::string> resources;
1033 context_.GetIndex().GetAllUuids(resources, ResourceType_Study);
1034
1035 std::set<std::string> years;
1036
1037 for (std::list<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it)
1038 {
1039 DicomMap tags;
1040 std::string studyDate;
1041 if (context_.GetIndex().GetMainDicomTags(tags, *it, ResourceType_Study, ResourceType_Study) &&
1042 tags.LookupStringValue(studyDate, DICOM_TAG_STUDY_DATE, false) &&
1043 studyDate.size() == 8)
1044 {
1045 years.insert(studyDate.substr(0, 4)); // Get the year from "YYYYMMDD"
1046 }
1047 }
1048
1049 for (std::set<std::string>::const_iterator it = years.begin(); it != years.end(); ++it)
1050 {
1051 target.AddResource(new IWebDavBucket::Folder(*it));
1052 }
1053
1054 return true;
1055 }
1056
1057 virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE
1058 {
1059 return new ListOfStudiesByMonth(context_, path, templates_);
1060 }
1061
1062 public:
1063 ListOfStudiesByYear(ServerContext& context,
1064 const Templates& templates) :
1065 context_(context),
1066 templates_(templates)
1067 {
1068 }
1069 };
1070
1071
1072 void OrthancWebDav::AddVirtualFile(Collection& collection,
1073 const UriComponents& path,
1074 const std::string& filename)
1075 {
1076 MimeType mime;
1077 std::string content;
1078 boost::posix_time::ptime modification;
1079
1080 UriComponents p = path;
1081 p.push_back(filename);
1082
1083 if (GetFileContent(mime, content, modification, p))
1084 {
1085 std::unique_ptr<File> f(new File(filename));
1086 f->SetMimeType(mime);
1087 f->SetContentLength(content.size());
1088 f->SetCreationTime(modification);
1089 collection.AddResource(f.release());
1090 }
1091 }
1092
1093
1094 void OrthancWebDav::UploadWorker(OrthancWebDav* that)
1095 {
1096 assert(that != NULL);
1097
1098 boost::posix_time::ptime lastModification = GetNow();
1099
1100 while (that->running_)
1101 {
1102 std::unique_ptr<IDynamicObject> obj(that->uploadQueue_.Dequeue(100));
1103 if (obj.get() != NULL)
1104 {
1105 that->Upload(reinterpret_cast<const SingleValueObject<std::string>&>(*obj).GetValue());
1106 lastModification = GetNow();
1107 }
1108 else if (GetNow() - lastModification > boost::posix_time::seconds(10))
1109 {
1110 // After every 10 seconds of inactivity, remove the empty folders
1111 LOG(INFO) << "Cleaning up the empty WebDAV upload folders";
1112 that->uploads_.RemoveEmptyFolders();
1113 lastModification = GetNow();
1114 }
1115 }
1116 }
1117
1118
1119 void OrthancWebDav::Upload(const std::string& path)
1120 {
1121 UriComponents uri;
1122 Toolbox::SplitUriComponents(uri, path);
1123
1124 LOG(INFO) << "Upload from WebDAV: " << path;
1125
1126 MimeType mime;
1127 std::string content;
1128 boost::posix_time::ptime time;
1129 if (uploads_.GetFileContent(mime, content, time, uri))
1130 {
1131 DicomInstanceToStore instance;
1132 // instance.SetOrigin(DicomInstanceOrigin_WebDav);
1133 instance.SetBuffer(content.c_str(), content.size());
1134
1135 std::string publicId;
1136 StoreStatus status = context_.Store(publicId, instance, StoreInstanceMode_Default);
1137 if (status == StoreStatus_Success ||
1138 status == StoreStatus_AlreadyStored)
1139 {
1140 LOG(INFO) << "Successfully imported DICOM instance from WebDAV: " << path << " (Orthanc ID: " << publicId << ")";
1141 uploads_.DeleteItem(uri);
1142 }
1143 else
1144 {
1145 LOG(WARNING) << "Cannot import DICOM instance from WebWAV: " << path;
1146 }
1147 }
1148 }
1149
1150
1151 OrthancWebDav::OrthancWebDav(ServerContext& context) :
1152 context_(context),
1153 uploads_(false /* store uploads as temporary files */),
1154 running_(false)
1155 {
1156 patientsTemplates_[ResourceType_Patient] = "{{PatientID}} - {{PatientName}}";
1157 patientsTemplates_[ResourceType_Study] = "{{StudyDate}} - {{StudyDescription}}";
1158 patientsTemplates_[ResourceType_Series] = "{{Modality}} - {{SeriesDescription}}";
1159
1160 studiesTemplates_[ResourceType_Study] = "{{PatientID}} - {{PatientName}} - {{StudyDescription}}";
1161 studiesTemplates_[ResourceType_Series] = patientsTemplates_[ResourceType_Series];
1162
1163 patients_.reset(new RootNode(context, ResourceType_Patient, patientsTemplates_));
1164 studies_.reset(new RootNode(context, ResourceType_Study, studiesTemplates_));
1165 dates_.reset(new ListOfStudiesByYear(context, studiesTemplates_));
1166 }
1167
1168
1169 bool OrthancWebDav::IsExistingFolder(const UriComponents& path)
1170 {
1171 if (path.empty())
1172 {
1173 return true;
1174 }
1175 else if (path[0] == BY_UIDS)
1176 {
1177 return (path.size() <= 3 &&
1178 (path.size() != 3 || path[2] != "study.json"));
1179 }
1180 else if (path[0] == BY_PATIENTS)
1181 {
1182 IWebDavBucket::Collection tmp;
1183 return patients_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end()));
1184 }
1185 else if (path[0] == BY_STUDIES)
1186 {
1187 IWebDavBucket::Collection tmp;
1188 return studies_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end()));
1189 }
1190 else if (path[0] == BY_DATE)
1191 {
1192 IWebDavBucket::Collection tmp;
1193 return dates_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end()));
1194 }
1195 else if (path[0] == UPLOADS)
1196 {
1197 return uploads_.IsExistingFolder(UriComponents(path.begin() + 1, path.end()));
1198 }
1199 else
1200 {
1201 return false;
1202 }
1203 }
1204
1205
1206 bool OrthancWebDav::ListCollection(Collection& collection,
1207 const UriComponents& path)
1208 {
1209 if (path.empty())
1210 {
1211 collection.AddResource(new Folder(BY_DATE));
1212 collection.AddResource(new Folder(BY_PATIENTS));
1213 collection.AddResource(new Folder(BY_STUDIES));
1214 collection.AddResource(new Folder(BY_UIDS));
1215 collection.AddResource(new Folder(UPLOADS));
1216 return true;
1217 }
1218 else if (path[0] == BY_UIDS)
1219 {
1220 DatabaseLookup query;
1221 ResourceType level;
1222 size_t limit = 0; // By default, no limits
1223
1224 if (path.size() == 1)
1225 {
1226 level = ResourceType_Study;
1227 limit = 100; // TODO
1228 }
1229 else if (path.size() == 2)
1230 {
1231 AddVirtualFile(collection, path, "study.json");
1232
1233 level = ResourceType_Series;
1234 query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
1235 true /* case sensitive */, true /* mandatory tag */);
1236 }
1237 else if (path.size() == 3)
1238 {
1239 AddVirtualFile(collection, path, "series.json");
1240
1241 level = ResourceType_Instance;
1242 query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
1243 true /* case sensitive */, true /* mandatory tag */);
1244 query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
1245 true /* case sensitive */, true /* mandatory tag */);
1246 }
1247 else
1248 {
1249 return false;
1250 }
1251
1252 DicomIdentifiersVisitor visitor(context_, collection, level);
1253 context_.Apply(visitor, query, level, 0 /* since */, limit);
1254
1255 return true;
1256 }
1257 else if (path[0] == BY_PATIENTS)
1258 {
1259 return patients_->ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
1260 }
1261 else if (path[0] == BY_STUDIES)
1262 {
1263 return studies_->ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
1264 }
1265 else if (path[0] == BY_DATE)
1266 {
1267 return dates_->ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
1268 }
1269 else if (path[0] == UPLOADS)
1270 {
1271 return uploads_.ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
1272 }
1273 else
1274 {
1275 return false;
1276 }
1277 }
1278
1279
1280 bool OrthancWebDav::GetFileContent(MimeType& mime,
1281 std::string& content,
1282 boost::posix_time::ptime& modificationTime,
1283 const UriComponents& path)
1284 {
1285 if (path.empty())
1286 {
1287 return false;
1288 }
1289 else if (path[0] == BY_UIDS)
1290 {
1291 if (path.size() == 3 &&
1292 path[2] == "study.json")
1293 {
1294 DatabaseLookup query;
1295 query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
1296 true /* case sensitive */, true /* mandatory tag */);
1297
1298 OrthancJsonVisitor visitor(context_, content, ResourceType_Study);
1299 context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
1300
1301 mime = MimeType_Json;
1302 return visitor.IsSuccess();
1303 }
1304 else if (path.size() == 4 &&
1305 path[3] == "series.json")
1306 {
1307 DatabaseLookup query;
1308 query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
1309 true /* case sensitive */, true /* mandatory tag */);
1310 query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
1311 true /* case sensitive */, true /* mandatory tag */);
1312
1313 OrthancJsonVisitor visitor(context_, content, ResourceType_Series);
1314 context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */);
1315
1316 mime = MimeType_Json;
1317 return visitor.IsSuccess();
1318 }
1319 else if (path.size() == 4 &&
1320 boost::ends_with(path[3], ".dcm"))
1321 {
1322 std::string sopInstanceUid = path[3];
1323 sopInstanceUid.resize(sopInstanceUid.size() - 4);
1324
1325 DatabaseLookup query;
1326 query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
1327 true /* case sensitive */, true /* mandatory tag */);
1328 query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
1329 true /* case sensitive */, true /* mandatory tag */);
1330 query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid,
1331 true /* case sensitive */, true /* mandatory tag */);
1332
1333 DicomFileVisitor visitor(context_, content, modificationTime);
1334 context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */);
1335
1336 mime = MimeType_Dicom;
1337 return visitor.IsSuccess();
1338 }
1339 else
1340 {
1341 return false;
1342 }
1343 }
1344 else if (path[0] == BY_PATIENTS)
1345 {
1346 return patients_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
1347 }
1348 else if (path[0] == BY_STUDIES)
1349 {
1350 return studies_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
1351 }
1352 else if (path[0] == UPLOADS)
1353 {
1354 return uploads_.GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
1355 }
1356 else
1357 {
1358 return false;
1359 }
1360 }
1361
1362
1363 bool OrthancWebDav::StoreFile(const std::string& content,
1364 const UriComponents& path)
1365 {
1366 if (path.size() >= 1 &&
1367 path[0] == UPLOADS)
1368 {
1369 UriComponents subpath(UriComponents(path.begin() + 1, path.end()));
1370
1371 if (uploads_.StoreFile(content, subpath))
1372 {
1373 if (!content.empty())
1374 {
1375 uploadQueue_.Enqueue(new SingleValueObject<std::string>(Toolbox::FlattenUri(subpath)));
1376 }
1377 return true;
1378 }
1379 else
1380 {
1381 return false;
1382 }
1383 }
1384 else
1385 {
1386 return false;
1387 }
1388 }
1389
1390
1391 bool OrthancWebDav::CreateFolder(const UriComponents& path)
1392 {
1393 if (path.size() >= 1 &&
1394 path[0] == UPLOADS)
1395 {
1396 return uploads_.CreateFolder(UriComponents(path.begin() + 1, path.end()));
1397 }
1398 else
1399 {
1400 return false;
1401 }
1402 }
1403
1404
1405 bool OrthancWebDav::DeleteItem(const std::vector<std::string>& path)
1406 {
1407 if (path.size() >= 1 &&
1408 path[0] == UPLOADS)
1409 {
1410 return uploads_.DeleteItem(UriComponents(path.begin() + 1, path.end()));
1411 }
1412 else
1413 {
1414 return false; // read-only
1415 }
1416 }
1417
1418
1419 void OrthancWebDav::Start()
1420 {
1421 if (running_)
1422 {
1423 throw OrthancException(ErrorCode_BadSequenceOfCalls);
1424 }
1425 else
1426 {
1427 LOG(INFO) << "Starting the WebDAV upload thread";
1428 running_ = true;
1429 uploadThread_ = boost::thread(UploadWorker, this);
1430 }
1431 }
1432
1433
1434 void OrthancWebDav::Stop()
1435 {
1436 if (running_)
1437 {
1438 LOG(INFO) << "Stopping the WebDAV upload thread";
1439 running_ = false;
1440 if (uploadThread_.joinable())
1441 {
1442 uploadThread_.join();
1443 }
1444 }
1445 }
1446 }