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-2021 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 Affero General Public License
9  * as published by the Free Software Foundation, either version 3 of
10  * the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program. If not, see <http://www.gnu.org/licenses/>.
19  **/
20 
21 
22 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
23 #include "Configuration.h"
24 #include "DicomWebFormatter.h"
25 
26 #include <Compatibility.h>
27 #include <ChunkedBuffer.h>
28 #include <Logging.h>
29 #include <Toolbox.h>
30 
31 #include <memory>
32 
33 
34 static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
35 static const char* const INSTANCES = "Instances";
36 static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
37 
38 
GetResourceUri(Orthanc::ResourceType level,const std::string & publicId)39 static std::string GetResourceUri(Orthanc::ResourceType level,
40                                   const std::string& publicId)
41 {
42   switch (level)
43   {
44     case Orthanc::ResourceType_Study:
45       return "/studies/" + publicId;
46 
47     case Orthanc::ResourceType_Series:
48       return "/series/" + publicId;
49 
50     case Orthanc::ResourceType_Instance:
51       return "/instances/" + publicId;
52 
53     default:
54       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
55   }
56 }
57 
58 
59 
AcceptMultipartDicom(bool & transcode,Orthanc::DicomTransferSyntax & targetSyntax,const OrthancPluginHttpRequest * request)60 static void AcceptMultipartDicom(bool& transcode,
61                                  Orthanc::DicomTransferSyntax& targetSyntax /* only if transcoding */,
62                                  const OrthancPluginHttpRequest* request)
63 {
64   /**
65    * Up to release 1.4 of the DICOMweb plugin, WADO-RS
66    * RetrieveInstance, RetrieveSeries and RetrieveStudy did *NOT*
67    * transcode if no transer syntax was explicitly provided. This was
68    * because the DICOM standard didn't specify a behavior in this case
69    * up to DICOM 2016b:
70    * http://dicom.nema.org/medical/dicom/2016b/output/chtml/part18/sect_6.5.3.html
71    *
72    * However, starting with DICOM 2016c, it is explicitly stated that
73    * "If transfer-syntax is not specified in the dcm-parameters the
74    * origin server shall use the Explicit VR Little Endian Transfer
75    * Syntax "1.2.840.10008.1.2.1" for each Instance":
76    * http://dicom.nema.org/medical/dicom/2016c/output/chtml/part18/sect_6.5.3.html
77    *
78    * As a consequence, starting with release 1.5 of the DICOMweb
79    * plugin, transcoding to "Little Endian Explicit" takes place by
80    * default. If this transcoding is not desirable, the "Accept" HTTP
81    * header can be set to
82    * "multipart/related;type=application/dicom;transfer-syntax=*" (not
83    * the asterisk "*") in order to prevent transcoding. The same
84    * convention is used by the Google Cloud Platform:
85    * https://cloud.google.com/healthcare/docs/dicom
86    **/
87   transcode = true;
88   targetSyntax = Orthanc::DicomTransferSyntax_LittleEndianExplicit;
89 
90   std::string accept;
91 
92   if (!OrthancPlugins::LookupHttpHeader(accept, request, "accept"))
93   {
94     return;   // By default, return "multipart/related; type=application/dicom;"
95   }
96 
97   std::string application;
98   std::map<std::string, std::string> attributes;
99   OrthancPlugins::ParseContentType(application, attributes, accept);
100 
101   if (application != "multipart/related" &&
102       application != "*/*")
103   {
104     throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
105                                     "This WADO-RS plugin cannot generate the following content type: " + accept);
106   }
107 
108   if (attributes.find("type") != attributes.end())
109   {
110     std::string s = attributes["type"];
111     Orthanc::Toolbox::ToLowerCase(s);
112     if (s != "application/dicom")
113     {
114       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
115                                       "This WADO-RS plugin only supports application/dicom "
116                                       "return type for DICOM retrieval (" + accept + ")");
117     }
118   }
119 
120   static const char* const TRANSFER_SYNTAX = "transfer-syntax";
121 
122   std::map<std::string, std::string>::const_iterator found = attributes.find(TRANSFER_SYNTAX);
123   if (found != attributes.end())
124   {
125     /**
126      * The "*" case below is related to Google Healthcare API:
127      * https://groups.google.com/d/msg/orthanc-users/w1Ekrsc6-U8/T2a_DoQ5CwAJ
128      **/
129     if (found->second == "*")
130     {
131       transcode = false;
132     }
133     else
134     {
135       transcode = true;
136 
137       if (!Orthanc::LookupTransferSyntax(targetSyntax, found->second))
138       {
139         throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
140                                         "Unsupported transfer syntax in WADO-RS: " + found->second);
141       }
142     }
143   }
144 }
145 
146 
147 
AcceptMetadata(const OrthancPluginHttpRequest * request,bool & isXml)148 static bool AcceptMetadata(const OrthancPluginHttpRequest* request,
149                            bool& isXml)
150 {
151   isXml = false;    // By default, return application/dicom+json
152 
153   std::string accept;
154   if (!OrthancPlugins::LookupHttpHeader(accept, request, "accept"))
155   {
156     return true;
157   }
158 
159   std::string application;
160   std::map<std::string, std::string> attributes;
161   OrthancPlugins::ParseContentType(application, attributes, accept);
162 
163   std::vector<std::string> applicationTokens;
164   Orthanc::Toolbox::TokenizeString(applicationTokens, application, ',');
165 
166   for (size_t i = 0; i < applicationTokens.size(); i++)
167   {
168     std::string token = Orthanc::Toolbox::StripSpaces(applicationTokens[i]);
169 
170     if (token == "application/json" ||
171         token == "application/dicom+json" ||
172         token == "*/*")
173     {
174       return true;
175     }
176   }
177 
178   if (application != "multipart/related")
179   {
180     throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
181                                     "This WADO-RS plugin cannot generate the following content type: " + accept);
182   }
183 
184   if (attributes.find("type") != attributes.end())
185   {
186     std::string s = attributes["type"];
187     Orthanc::Toolbox::ToLowerCase(s);
188     if (s == "application/dicom+xml")
189     {
190       isXml = true;
191     }
192     else
193     {
194       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
195                                       "This WADO-RS plugin only supports application/dicom+xml "
196                                       "type for multipart/related accept (" + accept + ")");
197     }
198   }
199   else
200   {
201     throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
202                                     "Missing \"type\" in multipart/related accept type (" + accept + ")");
203   }
204 
205   if (attributes.find("transfer-syntax") != attributes.end())
206   {
207     throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
208                                     "This WADO-RS plugin cannot change the transfer syntax to " +
209                                     attributes["transfer-syntax"]);
210   }
211 
212   return true;
213 }
214 
215 
216 
AcceptBulkData(const OrthancPluginHttpRequest * request)217 static bool AcceptBulkData(const OrthancPluginHttpRequest* request)
218 {
219   std::string accept;
220 
221   if (!OrthancPlugins::LookupHttpHeader(accept, request, "accept"))
222   {
223     return true;   // By default, return "multipart/related; type=application/octet-stream;"
224   }
225 
226   std::string application;
227   std::map<std::string, std::string> attributes;
228   OrthancPlugins::ParseContentType(application, attributes, accept);
229 
230   if (application != "multipart/related" &&
231       application != "*/*")
232   {
233     throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
234                                     "This WADO-RS plugin cannot generate the following "
235                                     "bulk data type: " + accept);
236   }
237 
238   if (attributes.find("type") != attributes.end())
239   {
240     std::string s = attributes["type"];
241     Orthanc::Toolbox::ToLowerCase(s);
242     if (s != "application/octet-stream")
243     {
244       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
245                                       "This WADO-RS plugin only supports application/octet-stream "
246                                       "return type for bulk data retrieval (" + accept + ")");
247     }
248   }
249 
250   if (attributes.find("ra,ge") != attributes.end())
251   {
252     throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
253                                     "This WADO-RS plugin does not support Range retrieval, "
254                                     "it can only return entire bulk data object");
255   }
256 
257   return true;
258 }
259 
260 
AnswerListOfDicomInstances(OrthancPluginRestOutput * output,Orthanc::ResourceType level,const std::string & publicId,bool transcode,Orthanc::DicomTransferSyntax targetSyntax)261 static void AnswerListOfDicomInstances(OrthancPluginRestOutput* output,
262                                        Orthanc::ResourceType level,
263                                        const std::string& publicId,
264                                        bool transcode,
265                                        Orthanc::DicomTransferSyntax targetSyntax /* only if transcoding */)
266 {
267   if (level != Orthanc::ResourceType_Study &&
268       level != Orthanc::ResourceType_Series &&
269       level != Orthanc::ResourceType_Instance)
270   {
271     throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
272   }
273 
274   OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
275 
276   Json::Value instances;
277 
278   if (level == Orthanc::ResourceType_Instance)
279   {
280     Json::Value tmp = Json::objectValue;
281     tmp["ID"] = publicId;
282 
283     instances = Json::arrayValue;
284     instances.append(tmp);
285   }
286   else
287   {
288     if (!OrthancPlugins::RestApiGet(instances, GetResourceUri(level, publicId) + "/instances", false))
289     {
290       // Internal error
291       OrthancPluginSendHttpStatusCode(context, output, 400);
292       return;
293     }
294   }
295 
296   if (OrthancPluginStartMultipartAnswer(context, output, "related", "application/dicom"))
297   {
298     throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
299   }
300 
301   for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
302   {
303     const std::string uri = "/instances/" + instances[i]["ID"].asString();
304 
305     bool transcodeThisInstance;
306 
307     std::string sourceTransferSyntax;
308     if (!transcode)
309     {
310       transcodeThisInstance = false;
311     }
312     else if (OrthancPlugins::RestApiGetString(sourceTransferSyntax, uri + "/metadata/TransferSyntax", false))
313     {
314       // Avoid transcoding if the source file already uses the expected transfer syntax
315       Orthanc::DicomTransferSyntax syntax;
316       if (Orthanc::LookupTransferSyntax(syntax, sourceTransferSyntax))
317       {
318         transcodeThisInstance = (syntax != targetSyntax);
319       }
320       else
321       {
322         transcodeThisInstance = true;
323       }
324     }
325     else
326     {
327       // The transfer syntax of the source file is unknown, transcode it to be sure
328       transcodeThisInstance = true;
329     }
330 
331     OrthancPlugins::MemoryBuffer dicom;
332     if (dicom.RestApiGet(uri + "/file", false))
333     {
334       if (transcodeThisInstance)
335       {
336         std::unique_ptr<OrthancPlugins::DicomInstance> transcoded(
337           OrthancPlugins::DicomInstance::Transcode(
338             dicom.GetData(), dicom.GetSize(), Orthanc::GetTransferSyntaxUid(targetSyntax)));
339 
340         if (OrthancPluginSendMultipartItem(
341               context, output, reinterpret_cast<const char*>(transcoded->GetBuffer()),
342               transcoded->GetSize()) != 0)
343         {
344           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
345         }
346       }
347       else
348       {
349         if (OrthancPluginSendMultipartItem(context, output, dicom.GetData(), dicom.GetSize()) != 0)
350         {
351           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
352         }
353       }
354     }
355   }
356 }
357 
358 
359 
360 namespace
361 {
362   class SetOfDicomInstances : public boost::noncopyable
363   {
364   private:
365     std::vector<Orthanc::DicomMap*>  instances_;
366 
367   public:
~SetOfDicomInstances()368     ~SetOfDicomInstances()
369     {
370       for (size_t i = 0; i < instances_.size(); i++)
371       {
372         assert(instances_[i] != NULL);
373         delete instances_[i];
374       }
375     }
376 
GetSize() const377     size_t GetSize() const
378     {
379       return instances_.size();
380     }
381 
ReadInstance(const std::string & publicId)382     bool ReadInstance(const std::string& publicId)
383     {
384       Json::Value dicomAsJson;
385 
386       if (OrthancPlugins::RestApiGet(dicomAsJson, "/instances/" + publicId + "/tags", false))
387       {
388         std::unique_ptr<Orthanc::DicomMap> instance(new Orthanc::DicomMap);
389         instance->FromDicomAsJson(dicomAsJson);
390         instances_.push_back(instance.release());
391 
392         return true;
393       }
394       else
395       {
396         return false;
397       }
398     }
399 
400 
MinorityReport(Orthanc::DicomMap & target,const Orthanc::DicomTag & tag) const401     void MinorityReport(Orthanc::DicomMap& target,
402                         const Orthanc::DicomTag& tag) const
403     {
404       typedef std::map<std::string, unsigned int>  Counters;
405 
406       Counters counters;
407 
408       for (size_t i = 0; i < instances_.size(); i++)
409       {
410         assert(instances_[i] != NULL);
411 
412         std::string value;
413         if (instances_[i]->LookupStringValue(value, tag, false))
414         {
415           Counters::iterator found = counters.find(value);
416           if (found == counters.end())
417           {
418             counters[value] = 1;
419           }
420           else
421           {
422             found->second ++;
423           }
424         }
425       }
426 
427       if (!counters.empty())
428       {
429         Counters::const_iterator current = counters.begin();
430 
431         std::string maxValue = current->first;
432         size_t maxCount = current->second;
433 
434         ++current;
435 
436         while (current != counters.end())
437         {
438           if (maxCount < current->second)
439           {
440             maxValue = current->first;
441             maxCount = current->second;
442           }
443 
444           ++current;
445         }
446 
447         target.SetValue(tag, maxValue, false);
448 
449         // Take the ceiling of the number of available instances
450         const size_t threshold = instances_.size() / 2 + 1;
451         if (maxCount < threshold)
452         {
453           LOG(WARNING) << "No consensus on the value of a tag during WADO-RS Retrieve "
454                        << "Metadata in Extrapolate mode: " << tag.Format();
455         }
456       }
457     }
458   };
459 
460 
461   class MainDicomTagsCache : public boost::noncopyable
462   {
463   private:
464     struct Info : public boost::noncopyable
465     {
466       Orthanc::DicomMap  dicom_;
467       std::string        parent_;
468     };
469 
470     typedef std::pair<std::string, Orthanc::ResourceType>  Index;
471     typedef std::map<Index, Info*>                         Content;
472 
473     Content  content_;
474 
ReadResource(Orthanc::DicomMap & dicom,std::string & parent,OrthancPlugins::MetadataMode mode,const std::string & orthancId,Orthanc::ResourceType level)475     static bool ReadResource(Orthanc::DicomMap& dicom,
476                              std::string& parent,
477                              OrthancPlugins::MetadataMode mode,
478                              const std::string& orthancId,
479                              Orthanc::ResourceType level)
480     {
481       std::string uri;
482       std::string parentField;
483 
484       switch (level)
485       {
486         case Orthanc::ResourceType_Study:
487           uri = "/studies/" + orthancId;
488           break;
489 
490         case Orthanc::ResourceType_Series:
491           uri = "/series/" + orthancId;
492           parentField = "ParentStudy";
493           break;
494 
495         case Orthanc::ResourceType_Instance:
496           uri = "/instances/" + orthancId;
497           parentField = "ParentSeries";
498           break;
499 
500         default:
501           throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
502       }
503 
504       Json::Value value;
505       if (!OrthancPlugins::RestApiGet(value, uri, false))
506       {
507         return false;
508       }
509 
510 
511       if (value.type() != Json::objectValue ||
512           !value.isMember(MAIN_DICOM_TAGS))
513       {
514         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
515       }
516 
517       dicom.ParseMainDicomTags(value[MAIN_DICOM_TAGS], level);
518 
519       if (level == Orthanc::ResourceType_Study)
520       {
521         if (value.isMember(PATIENT_MAIN_DICOM_TAGS))
522         {
523           dicom.ParseMainDicomTags(value[PATIENT_MAIN_DICOM_TAGS], Orthanc::ResourceType_Patient);
524         }
525         else
526         {
527           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
528         }
529       }
530 
531       if (!parentField.empty())
532       {
533         if (value.isMember(parentField) &&
534             value[parentField].type() == Json::stringValue)
535         {
536           parent = value[parentField].asString();
537         }
538         else
539         {
540           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
541         }
542       }
543 
544 
545       if (mode == OrthancPlugins::MetadataMode_Extrapolate &&
546           (level == Orthanc::ResourceType_Series ||
547            level == Orthanc::ResourceType_Study))
548       {
549         std::set<Orthanc::DicomTag> tags;
550         OrthancPlugins::Configuration::GetExtrapolatedMetadataTags(tags, level);
551 
552         if (!tags.empty())
553         {
554           /**
555            * Complete the series/study-level tags, with instance-level
556            * tags that are not considered as "main DICOM tags" in
557            * Orthanc, but that are necessary for Web viewers, and that
558            * are expected to be constant throughout all the instances of
559            * the study/series. To this end, we read up to "N" DICOM
560            * instances of this study/series from disk, and for the tags
561            * of interest, we look at whether there is a consensus in the
562            * value among these instances. Obviously, this is an
563            * approximation to improve performance.
564            **/
565 
566           std::set<std::string> allInstances;
567 
568           switch (level)
569           {
570             case Orthanc::ResourceType_Series:
571               if (!value.isMember(INSTANCES) ||
572                   value[INSTANCES].type() != Json::arrayValue)
573               {
574                 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
575               }
576               else
577               {
578                 for (Json::Value::ArrayIndex i = 0; i < value[INSTANCES].size(); i++)
579                 {
580                   if (value[INSTANCES][i].type() != Json::stringValue)
581                   {
582                     throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
583                   }
584                   else
585                   {
586                     allInstances.insert(value[INSTANCES][i].asString());
587                   }
588                 }
589               }
590 
591               break;
592 
593             case Orthanc::ResourceType_Study:
594             {
595               Json::Value tmp;
596               if (OrthancPlugins::RestApiGet(tmp, "/studies/" + orthancId + "/instances", false))
597               {
598                 if (tmp.type() != Json::arrayValue)
599                 {
600                   throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
601                 }
602 
603                 for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
604                 {
605                   if (tmp[i].type() != Json::objectValue ||
606                       !tmp[i].isMember("ID") ||
607                       tmp[i]["ID"].type() != Json::stringValue)
608                   {
609                     throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
610                   }
611                   else
612                   {
613                     allInstances.insert(tmp[i]["ID"].asString());
614                   }
615                 }
616               }
617 
618               break;
619             }
620 
621             default:
622               throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
623           }
624 
625 
626           // Select up to N random instances. The instances are
627           // implicitly selected randomly, as the public ID of an
628           // instance is a SHA-1 hash (whose domain is uniformly distributed)
629 
630           static const size_t N = 3;
631           SetOfDicomInstances selectedInstances;
632 
633           for (std::set<std::string>::const_iterator it = allInstances.begin();
634                selectedInstances.GetSize() < N && it != allInstances.end(); ++it)
635           {
636             selectedInstances.ReadInstance(*it);
637           }
638 
639           for (std::set<Orthanc::DicomTag>::const_iterator
640                  it = tags.begin(); it != tags.end(); ++it)
641           {
642             selectedInstances.MinorityReport(dicom, *it);
643           }
644         }
645       }
646 
647       return true;
648     }
649 
650 
Lookup(Orthanc::DicomMap & dicom,std::string & parent,OrthancPlugins::MetadataMode mode,const std::string & orthancId,Orthanc::ResourceType level)651     bool Lookup(Orthanc::DicomMap& dicom,
652                 std::string& parent,
653                 OrthancPlugins::MetadataMode mode,
654                 const std::string& orthancId,
655                 Orthanc::ResourceType level)
656     {
657       Content::iterator found = content_.find(std::make_pair(orthancId, level));
658 
659       if (found == content_.end())
660       {
661         std::unique_ptr<Info> info(new Info);
662         if (!ReadResource(info->dicom_, info->parent_, mode, orthancId, level))
663         {
664           return false;
665         }
666 
667         found = content_.insert(std::make_pair(std::make_pair(orthancId, level), info.release())).first;
668       }
669 
670       assert(found != content_.end() &&
671              found->second != NULL);
672       dicom.Merge(found->second->dicom_);
673       parent = found->second->parent_;
674 
675       return true;
676     }
677 
678 
679   public:
~MainDicomTagsCache()680     ~MainDicomTagsCache()
681     {
682       for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
683       {
684         assert(it->second != NULL);
685         delete it->second;
686       }
687     }
688 
689 
GetInstance(Orthanc::DicomMap & dicom,OrthancPlugins::MetadataMode mode,const std::string & orthancId)690     bool GetInstance(Orthanc::DicomMap& dicom,
691                      OrthancPlugins::MetadataMode mode,
692                      const std::string& orthancId)
693     {
694       std::string seriesId, studyId, nope;
695 
696       return (ReadResource(dicom, seriesId, mode, orthancId, Orthanc::ResourceType_Instance) &&
697               Lookup(dicom, studyId, mode, seriesId, Orthanc::ResourceType_Series) &&
698               Lookup(dicom, nope /* patient id is unused */, mode, studyId, Orthanc::ResourceType_Study));
699     }
700   };
701 }
702 
703 
704 
WriteInstanceMetadata(OrthancPlugins::DicomWebFormatter::HttpWriter & writer,OrthancPlugins::MetadataMode mode,MainDicomTagsCache & cache,const std::string & orthancId,const std::string & studyInstanceUid,const std::string & seriesInstanceUid,const std::string & sopInstanceUid,const std::string & wadoBase)705 static void WriteInstanceMetadata(OrthancPlugins::DicomWebFormatter::HttpWriter& writer,
706                                   OrthancPlugins::MetadataMode mode,
707                                   MainDicomTagsCache& cache,
708                                   const std::string& orthancId,
709                                   const std::string& studyInstanceUid,
710                                   const std::string& seriesInstanceUid,
711                                   const std::string& sopInstanceUid,
712                                   const std::string& wadoBase)
713 {
714   assert(!orthancId.empty() &&
715          !studyInstanceUid.empty() &&
716          !seriesInstanceUid.empty() &&
717          !sopInstanceUid.empty() &&
718          !wadoBase.empty());
719 
720   const std::string bulkRoot = (wadoBase +
721                                 "studies/" + studyInstanceUid +
722                                 "/series/" + seriesInstanceUid +
723                                 "/instances/" + sopInstanceUid + "/bulk");
724 
725   switch (mode)
726   {
727     case OrthancPlugins::MetadataMode_MainDicomTags:
728     case OrthancPlugins::MetadataMode_Extrapolate:
729     {
730       Orthanc::DicomMap dicom;
731       if (cache.GetInstance(dicom, mode, orthancId))
732       {
733         writer.AddOrthancMap(dicom);
734       }
735 
736       break;
737     }
738 
739     case OrthancPlugins::MetadataMode_Full:
740     {
741       // On a SSD drive, this version is twice slower than if using
742       // cache (see below)
743 
744       OrthancPlugins::MemoryBuffer dicom;
745       if (dicom.RestApiGet("/instances/" + orthancId + "/file", false))
746       {
747         writer.AddDicom(dicom.GetData(), dicom.GetSize(), bulkRoot);
748       }
749 
750       break;
751     }
752 
753     default:
754       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
755   }
756 
757 
758 #if 0
759   /**
760    **/
761 
762   // TODO - Have a global setting to enable/disable caching of DICOMweb
763 
764   // TODO - Have a way to clear the "4444" attachments if Orthanc
765   // version changes => Store Orthanc core version in a prefix or in
766   // another attachment?
767 
768   OrthancPlugins::MemoryBuffer buffer;
769 
770   if (writer.IsXml())
771   {
772     // DICOMweb XML is not cached
773     if (buffer.RestApiGet("/instances/" + orthancId + "/file", false))
774     {
775       writer.AddDicom(buffer.GetData(), buffer.GetSize(), bulkRoot);
776     }
777   }
778   else
779   {
780     if (buffer.RestApiGet("/instances/" + orthancId + "/attachments/4444/data", false))
781     {
782       writer.AddDicomWebSerializedJson(buffer.GetData(), buffer.GetSize());
783     }
784     else if (buffer.RestApiGet("/instances/" + orthancId + "/file", false))
785     {
786       // "Ignore binary mode" in DICOMweb conversion if caching is
787       // enabled, as the bulk root can change across executions
788 
789       std::string dicomweb;
790       {
791         // TODO - Avoid a global mutex => Need to change Orthanc SDK
792         OrthancPlugins::DicomWebFormatter::Apply(
793           dicomweb, OrthancPlugins::GetGlobalContext(), buffer.GetData(), buffer.GetSize(),
794           false /* JSON */, OrthancPluginDicomWebBinaryMode_Ignore, "");
795       }
796 
797       buffer.RestApiPut("/instances/" + orthancId + "/attachments/4444", dicomweb, false);
798       writer.AddDicomWebSerializedJson(dicomweb.c_str(), dicomweb.size());
799     }
800   }
801 #endif
802 }
803 
804 
805 
LocateStudy(OrthancPluginRestOutput * output,std::string & orthancId,std::string & studyInstanceUid,const OrthancPluginHttpRequest * request)806 bool LocateStudy(OrthancPluginRestOutput* output,
807                  std::string& orthancId,
808                  std::string& studyInstanceUid,
809                  const OrthancPluginHttpRequest* request)
810 {
811   OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
812 
813   if (request->method != OrthancPluginHttpMethod_Get)
814   {
815     OrthancPluginSendMethodNotAllowed(context, output, "GET");
816     return false;
817   }
818 
819   studyInstanceUid = request->groups[0];
820 
821   try
822   {
823     OrthancPlugins::OrthancString tmp;
824     tmp.Assign(OrthancPluginLookupStudy(context, studyInstanceUid.c_str()));
825 
826     if (tmp.GetContent() != NULL)
827     {
828       tmp.ToString(orthancId);
829       return true;
830     }
831   }
832   catch (Orthanc::OrthancException&)
833   {
834   }
835 
836   throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
837                                   "Accessing an inexistent study with WADO-RS: " + studyInstanceUid);
838 }
839 
840 
LocateSeries(OrthancPluginRestOutput * output,std::string & orthancId,std::string & studyInstanceUid,std::string & seriesInstanceUid,const OrthancPluginHttpRequest * request)841 bool LocateSeries(OrthancPluginRestOutput* output,
842                   std::string& orthancId,
843                   std::string& studyInstanceUid,
844                   std::string& seriesInstanceUid,
845                   const OrthancPluginHttpRequest* request)
846 {
847   OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
848 
849   if (request->method != OrthancPluginHttpMethod_Get)
850   {
851     OrthancPluginSendMethodNotAllowed(context, output, "GET");
852     return false;
853   }
854 
855   studyInstanceUid = request->groups[0];
856   seriesInstanceUid = request->groups[1];
857 
858   bool found = false;
859 
860   try
861   {
862     OrthancPlugins::OrthancString tmp;
863     tmp.Assign(OrthancPluginLookupSeries(context, seriesInstanceUid.c_str()));
864 
865     if (tmp.GetContent() != NULL)
866     {
867       tmp.ToString(orthancId);
868       found = true;
869     }
870   }
871   catch (Orthanc::OrthancException&)
872   {
873   }
874 
875   if (!found)
876   {
877     throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
878                                     "Accessing an inexistent series with WADO-RS: " + seriesInstanceUid);
879   }
880 
881   Json::Value study;
882   if (!OrthancPlugins::RestApiGet(study, "/series/" + orthancId + "/study", false))
883   {
884     OrthancPluginSendHttpStatusCode(context, output, 404);
885     return false;
886   }
887   else if (study[MAIN_DICOM_TAGS]["StudyInstanceUID"].asString() != studyInstanceUid)
888   {
889     throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
890                                     "No series " + seriesInstanceUid + " in study " + studyInstanceUid);
891   }
892   else
893   {
894     return true;
895   }
896 }
897 
898 
LocateInstance(OrthancPluginRestOutput * output,std::string & orthancId,std::string & studyInstanceUid,std::string & seriesInstanceUid,std::string & sopInstanceUid,const OrthancPluginHttpRequest * request)899 bool LocateInstance(OrthancPluginRestOutput* output,
900                     std::string& orthancId,
901                     std::string& studyInstanceUid,
902                     std::string& seriesInstanceUid,
903                     std::string& sopInstanceUid,
904                     const OrthancPluginHttpRequest* request)
905 {
906   OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
907 
908   if (request->method != OrthancPluginHttpMethod_Get)
909   {
910     OrthancPluginSendMethodNotAllowed(context, output, "GET");
911     return false;
912   }
913 
914   studyInstanceUid = request->groups[0];
915   seriesInstanceUid = request->groups[1];
916   sopInstanceUid = request->groups[2];
917 
918   {
919     OrthancPlugins::OrthancString tmp;
920     tmp.Assign(OrthancPluginLookupInstance(context, sopInstanceUid.c_str()));
921 
922     if (tmp.GetContent() == NULL)
923     {
924       throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
925                                       "Accessing an inexistent instance with WADO-RS: " + sopInstanceUid);
926     }
927 
928     tmp.ToString(orthancId);
929   }
930 
931   Json::Value study, series;
932   if (!OrthancPlugins::RestApiGet(series, "/instances/" + orthancId + "/series", false) ||
933       !OrthancPlugins::RestApiGet(study, "/instances/" + orthancId + "/study", false))
934   {
935     OrthancPluginSendHttpStatusCode(context, output, 404);
936     return false;
937   }
938   else if (study[MAIN_DICOM_TAGS]["StudyInstanceUID"].asString() != studyInstanceUid ||
939            series[MAIN_DICOM_TAGS]["SeriesInstanceUID"].asString() != seriesInstanceUid)
940   {
941     throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
942                                     "Instance " + sopInstanceUid +
943                                     " is not both in study " + studyInstanceUid +
944                                     " and in series " + seriesInstanceUid);
945   }
946   else
947   {
948     return true;
949   }
950 }
951 
952 
RetrieveDicomStudy(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)953 void RetrieveDicomStudy(OrthancPluginRestOutput* output,
954                         const char* url,
955                         const OrthancPluginHttpRequest* request)
956 {
957   bool transcode;
958   Orthanc::DicomTransferSyntax targetSyntax;
959 
960   AcceptMultipartDicom(transcode, targetSyntax, request);
961 
962   std::string orthancId, studyInstanceUid;
963   if (LocateStudy(output, orthancId, studyInstanceUid, request))
964   {
965     AnswerListOfDicomInstances(output, Orthanc::ResourceType_Study, orthancId, transcode, targetSyntax);
966   }
967 }
968 
969 
RetrieveDicomSeries(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)970 void RetrieveDicomSeries(OrthancPluginRestOutput* output,
971                          const char* url,
972                          const OrthancPluginHttpRequest* request)
973 {
974   bool transcode;
975   Orthanc::DicomTransferSyntax targetSyntax;
976 
977   AcceptMultipartDicom(transcode, targetSyntax, request);
978 
979   std::string orthancId, studyInstanceUid, seriesInstanceUid;
980   if (LocateSeries(output, orthancId, studyInstanceUid, seriesInstanceUid, request))
981   {
982     AnswerListOfDicomInstances(output, Orthanc::ResourceType_Series, orthancId, transcode, targetSyntax);
983   }
984 }
985 
986 
987 
RetrieveDicomInstance(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)988 void RetrieveDicomInstance(OrthancPluginRestOutput* output,
989                            const char* url,
990                            const OrthancPluginHttpRequest* request)
991 {
992   bool transcode;
993   Orthanc::DicomTransferSyntax targetSyntax;
994 
995   AcceptMultipartDicom(transcode, targetSyntax, request);
996 
997   std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
998   if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request))
999   {
1000     AnswerListOfDicomInstances(output, Orthanc::ResourceType_Instance, orthancId, transcode, targetSyntax);
1001   }
1002 }
1003 
1004 
1005 
1006 namespace
1007 {
1008   class Identifier
1009   {
1010   private:
1011     std::string  orthancId_;
1012     std::string  dicomUid_;
1013 
1014   public:
Identifier(const std::string & orthancId,const std::string & dicomUid)1015     Identifier(const std::string& orthancId,
1016                const std::string& dicomUid) :
1017       orthancId_(orthancId),
1018       dicomUid_(dicomUid)
1019     {
1020     }
1021 
GetOrthancId() const1022     const std::string& GetOrthancId() const
1023     {
1024       return orthancId_;
1025     }
1026 
GetDicomUid() const1027     const std::string& GetDicomUid() const
1028     {
1029       return dicomUid_;
1030     }
1031   };
1032 }
1033 
1034 
GetChildrenIdentifiers(std::list<Identifier> & target,Orthanc::ResourceType level,const std::string & orthancId)1035 static void GetChildrenIdentifiers(std::list<Identifier>& target,
1036                                    Orthanc::ResourceType level,
1037                                    const std::string& orthancId)
1038 {
1039   static const char* const ID = "ID";
1040   static const char* const SERIES_INSTANCE_UID = "SeriesInstanceUID";
1041   static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
1042 
1043   target.clear();
1044 
1045   const char* tag = NULL;
1046   std::string uri;
1047 
1048   switch (level)
1049   {
1050     case Orthanc::ResourceType_Study:
1051       uri = "/studies/" + orthancId + "/series";
1052       tag = SERIES_INSTANCE_UID;
1053       break;
1054 
1055     case Orthanc::ResourceType_Series:
1056       uri = "/series/" + orthancId + "/instances";
1057       tag = SOP_INSTANCE_UID;
1058       break;
1059 
1060     default:
1061       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
1062   }
1063 
1064   assert(tag != NULL);
1065 
1066   Json::Value children;
1067   if (OrthancPlugins::RestApiGet(children, uri, false))
1068   {
1069     if (children.type() != Json::arrayValue)
1070     {
1071       throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
1072     }
1073 
1074     for (Json::Value::ArrayIndex i = 0; i < children.size(); i++)
1075     {
1076       if (children[i].type() != Json::objectValue ||
1077           !children[i].isMember(ID) ||
1078           !children[i].isMember(MAIN_DICOM_TAGS) ||
1079           children[i][ID].type() != Json::stringValue ||
1080           children[i][MAIN_DICOM_TAGS].type() != Json::objectValue ||
1081           !children[i][MAIN_DICOM_TAGS].isMember(tag) ||
1082           children[i][MAIN_DICOM_TAGS][tag].type() != Json::stringValue)
1083       {
1084         throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
1085       }
1086       else
1087       {
1088         target.push_back(Identifier(children[i][ID].asString(),
1089                                     children[i][MAIN_DICOM_TAGS][tag].asString()));
1090 
1091       }
1092     }
1093   }
1094 }
1095 
1096 
1097 
RetrieveStudyMetadata(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)1098 void RetrieveStudyMetadata(OrthancPluginRestOutput* output,
1099                            const char* url,
1100                            const OrthancPluginHttpRequest* request)
1101 {
1102   bool isXml;
1103   if (!AcceptMetadata(request, isXml))
1104   {
1105     OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 400 /* Bad request */);
1106   }
1107   else
1108   {
1109     const OrthancPlugins::MetadataMode mode =
1110       OrthancPlugins::Configuration::GetMetadataMode(Orthanc::ResourceType_Study);
1111 
1112     MainDicomTagsCache cache;
1113 
1114     std::string studyOrthancId, studyInstanceUid;
1115     if (LocateStudy(output, studyOrthancId, studyInstanceUid, request))
1116     {
1117       OrthancPlugins::DicomWebFormatter::HttpWriter writer(output, isXml);
1118 
1119       std::list<Identifier> series;
1120       GetChildrenIdentifiers(series, Orthanc::ResourceType_Study, studyOrthancId);
1121 
1122       for (std::list<Identifier>::const_iterator a = series.begin(); a != series.end(); ++a)
1123       {
1124         std::list<Identifier> instances;
1125         GetChildrenIdentifiers(instances, Orthanc::ResourceType_Series, a->GetOrthancId());
1126 
1127         for (std::list<Identifier>::const_iterator b = instances.begin(); b != instances.end(); ++b)
1128         {
1129           WriteInstanceMetadata(writer, mode, cache, b->GetOrthancId(), studyInstanceUid, a->GetDicomUid(),
1130                                 b->GetDicomUid(), OrthancPlugins::Configuration::GetBaseUrl(request));
1131         }
1132       }
1133 
1134       writer.Send();
1135     }
1136   }
1137 }
1138 
1139 
RetrieveSeriesMetadata(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)1140 void RetrieveSeriesMetadata(OrthancPluginRestOutput* output,
1141                             const char* url,
1142                             const OrthancPluginHttpRequest* request)
1143 {
1144   OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
1145 
1146   bool isXml;
1147   if (!AcceptMetadata(request, isXml))
1148   {
1149     OrthancPluginSendHttpStatusCode(context, output, 400 /* Bad request */);
1150   }
1151   else
1152   {
1153     const OrthancPlugins::MetadataMode mode =
1154       OrthancPlugins::Configuration::GetMetadataMode(Orthanc::ResourceType_Series);
1155 
1156     MainDicomTagsCache cache;
1157 
1158     std::string seriesOrthancId, studyInstanceUid, seriesInstanceUid;
1159     if (LocateSeries(output, seriesOrthancId, studyInstanceUid, seriesInstanceUid, request))
1160     {
1161       OrthancPlugins::DicomWebFormatter::HttpWriter writer(output, isXml);
1162 
1163       std::list<Identifier> instances;
1164       GetChildrenIdentifiers(instances, Orthanc::ResourceType_Series, seriesOrthancId);
1165 
1166       for (std::list<Identifier>::const_iterator a = instances.begin(); a != instances.end(); ++a)
1167       {
1168         WriteInstanceMetadata(writer, mode, cache, a->GetOrthancId(), studyInstanceUid, seriesInstanceUid,
1169                               a->GetDicomUid(), OrthancPlugins::Configuration::GetBaseUrl(request));
1170       }
1171 
1172       writer.Send();
1173     }
1174   }
1175 }
1176 
1177 
RetrieveInstanceMetadata(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)1178 void RetrieveInstanceMetadata(OrthancPluginRestOutput* output,
1179                               const char* url,
1180                               const OrthancPluginHttpRequest* request)
1181 {
1182   bool isXml;
1183   if (!AcceptMetadata(request, isXml))
1184   {
1185     OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 400 /* Bad request */);
1186   }
1187   else
1188   {
1189     MainDicomTagsCache cache;
1190 
1191     std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
1192     if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request))
1193     {
1194       OrthancPlugins::DicomWebFormatter::HttpWriter writer(output, isXml);
1195       WriteInstanceMetadata(writer, OrthancPlugins::MetadataMode_Full, cache, orthancId, studyInstanceUid,
1196                             seriesInstanceUid, sopInstanceUid, OrthancPlugins::Configuration::GetBaseUrl(request));
1197       writer.Send();
1198     }
1199   }
1200 }
1201 
1202 
RetrieveBulkData(OrthancPluginRestOutput * output,const char * url,const OrthancPluginHttpRequest * request)1203 void RetrieveBulkData(OrthancPluginRestOutput* output,
1204                       const char* url,
1205                       const OrthancPluginHttpRequest* request)
1206 {
1207   OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
1208 
1209   if (!AcceptBulkData(request))
1210   {
1211     OrthancPluginSendHttpStatusCode(context, output, 400 /* Bad request */);
1212     return;
1213   }
1214 
1215   std::string orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
1216   OrthancPlugins::MemoryBuffer content;
1217   if (LocateInstance(output, orthancId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request) &&
1218       content.RestApiGet("/instances/" + orthancId + "/file", false))
1219   {
1220     std::string bulk(request->groups[3]);
1221 
1222     std::vector<std::string> path;
1223     Orthanc::Toolbox::TokenizeString(path, bulk, '/');
1224 
1225     // Map the bulk data URI to the Orthanc "/instances/.../content/..." built-in URI
1226     std::string orthanc = "/instances/" + orthancId + "/content";
1227 
1228     Orthanc::DicomTag tmp(0, 0);
1229 
1230     if (path.size() == 1 &&
1231         Orthanc::DicomTag::ParseHexadecimal(tmp, path[0].c_str()) &&
1232         tmp == Orthanc::DICOM_TAG_PIXEL_DATA)
1233     {
1234       // Accessing pixel data: Return the raw content of the fragments in a multipart stream.
1235       // TODO - Is this how DICOMweb should work?
1236       orthanc += "/" + Orthanc::DICOM_TAG_PIXEL_DATA.Format();
1237 
1238       Json::Value frames;
1239       if (OrthancPlugins::RestApiGet(frames, orthanc, false))
1240       {
1241         if (frames.type() != Json::arrayValue ||
1242             OrthancPluginStartMultipartAnswer(context, output, "related", "application/octet-stream") != 0)
1243         {
1244           throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
1245         }
1246 
1247         for (Json::Value::ArrayIndex i = 0; i < frames.size(); i++)
1248         {
1249           std::string frame;
1250 
1251           if (frames[i].type() != Json::stringValue ||
1252               !OrthancPlugins::RestApiGetString(frame, orthanc + "/" + frames[i].asString(), false) ||
1253               OrthancPluginSendMultipartItem(context, output, frame.c_str(), frame.size()) != 0)
1254           {
1255             throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
1256           }
1257         }
1258       }
1259       else
1260       {
1261         throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
1262       }
1263     }
1264     else
1265     {
1266       if (path.size() % 2 != 1)
1267       {
1268         throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
1269                                         "Bulk data URI in WADO-RS should have an odd number of items: " + bulk);
1270       }
1271 
1272       for (size_t i = 0; i < path.size() / 2; i++)
1273       {
1274         int index;
1275 
1276         try
1277         {
1278           index = boost::lexical_cast<int>(path[2 * i + 1]);
1279         }
1280         catch (boost::bad_lexical_cast&)
1281         {
1282           throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest,
1283                                           "Bad sequence index in bulk data URI: " + bulk);
1284         }
1285 
1286         orthanc += "/" + path[2 * i] + "/" + boost::lexical_cast<std::string>(index - 1);
1287       }
1288 
1289       orthanc += "/" + path.back();
1290 
1291       std::string result;
1292       if (OrthancPlugins::RestApiGetString(result, orthanc, false))
1293       {
1294         if (OrthancPluginStartMultipartAnswer(context, output, "related", "application/octet-stream") != 0 ||
1295             OrthancPluginSendMultipartItem(context, output, result.c_str(), result.size()) != 0)
1296         {
1297           throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
1298         }
1299       }
1300       else
1301       {
1302         throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
1303       }
1304     }
1305   }
1306 }
1307