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 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 "../PrecompiledHeadersServer.h"
35 #include "ResourceModificationJob.h"
36 
37 #include "../../../OrthancFramework/Sources/Logging.h"
38 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
39 #include "../ServerContext.h"
40 
41 #include <dcmtk/dcmdata/dcfilefo.h>
42 #include <dcmtk/dcmdata/dcdeftag.h>
43 #include <cassert>
44 
45 namespace Orthanc
46 {
FormatResource(Json::Value & target,ResourceType level,const std::string & id)47   static void FormatResource(Json::Value& target,
48                              ResourceType level,
49                              const std::string& id)
50   {
51     target["Type"] = EnumerationToString(level);
52     target["ID"] = id;
53     target["Path"] = GetBasePath(level, id);
54   }
55 
56   class ResourceModificationJob::SingleOutput : public IOutput
57   {
58   private:
59     ResourceType  level_;
60     bool          isFirst_;
61     std::string   id_;
62     std::string   patientId_;
63 
64   public:
SingleOutput(ResourceType level)65     explicit SingleOutput(ResourceType level) :
66       level_(level),
67       isFirst_(true)
68     {
69       if (level_ != ResourceType_Patient &&
70           level_ != ResourceType_Study &&
71           level_ != ResourceType_Series)
72       {
73         throw OrthancException(ErrorCode_ParameterOutOfRange);
74       }
75     }
76 
Update(DicomInstanceHasher & hasher)77     virtual void Update(DicomInstanceHasher& hasher) ORTHANC_OVERRIDE
78     {
79       if (isFirst_)
80       {
81         switch (level_)
82         {
83           case ResourceType_Series:
84             id_ = hasher.HashSeries();
85             break;
86 
87           case ResourceType_Study:
88             id_ = hasher.HashStudy();
89             break;
90 
91           case ResourceType_Patient:
92             id_ = hasher.HashPatient();
93             break;
94 
95           default:
96             throw OrthancException(ErrorCode_InternalError);
97         }
98 
99         patientId_ = hasher.HashPatient();
100         isFirst_ = false;
101       }
102     }
103 
Format(Json::Value & target) const104     virtual void Format(Json::Value& target) const ORTHANC_OVERRIDE
105     {
106       assert(target.type() == Json::objectValue);
107 
108       if (!isFirst_)
109       {
110         FormatResource(target, level_, id_);
111         target["PatientID"] = patientId_;
112       }
113     }
114 
IsSingleResource() const115     virtual bool IsSingleResource() const ORTHANC_OVERRIDE
116     {
117       return true;
118     }
119 
GetLevel() const120     ResourceType GetLevel() const
121     {
122       return level_;
123     }
124   };
125 
126 
127   class ResourceModificationJob::MultipleOutputs : public IOutput
128   {
129   private:
FormatResources(Json::Value & target,ResourceType level,const std::set<std::string> & resources)130     static void FormatResources(Json::Value& target,
131                                 ResourceType level,
132                                 const std::set<std::string>& resources)
133     {
134       assert(target.type() == Json::arrayValue);
135 
136       for (std::set<std::string>::const_iterator
137              it = resources.begin(); it != resources.end(); ++it)
138       {
139         Json::Value item = Json::objectValue;
140         FormatResource(item, level, *it);
141         target.append(item);
142       }
143     }
144 
145     std::set<std::string>  instances_;
146     std::set<std::string>  series_;
147     std::set<std::string>  studies_;
148     std::set<std::string>  patients_;
149 
150   public:
Update(DicomInstanceHasher & hasher)151     virtual void Update(DicomInstanceHasher& hasher) ORTHANC_OVERRIDE
152     {
153       instances_.insert(hasher.HashInstance());
154       series_.insert(hasher.HashSeries());
155       studies_.insert(hasher.HashStudy());
156       patients_.insert(hasher.HashPatient());
157     }
158 
Format(Json::Value & target) const159     virtual void Format(Json::Value& target) const ORTHANC_OVERRIDE
160     {
161       assert(target.type() == Json::objectValue);
162       Json::Value resources = Json::arrayValue;
163       FormatResources(resources, ResourceType_Instance, instances_);
164       FormatResources(resources, ResourceType_Series, series_);
165       FormatResources(resources, ResourceType_Study, studies_);
166       FormatResources(resources, ResourceType_Patient, patients_);
167       target["Resources"] = resources;
168     }
169 
IsSingleResource() const170     virtual bool IsSingleResource() const ORTHANC_OVERRIDE
171     {
172       return false;
173     }
174   };
175 
176 
177 
HandleInstance(const std::string & instance)178   bool ResourceModificationJob::HandleInstance(const std::string& instance)
179   {
180     if (modification_.get() == NULL ||
181         output_.get() == NULL)
182     {
183       throw OrthancException(ErrorCode_BadSequenceOfCalls,
184                              "No modification was provided for this job");
185     }
186 
187 
188     LOG(INFO) << "Modifying instance in a job: " << instance;
189 
190 
191     /**
192      * Retrieve the original instance from the DICOM cache.
193      **/
194 
195     std::unique_ptr<DicomInstanceHasher> originalHasher;
196     std::unique_ptr<ParsedDicomFile> modified;
197 
198     try
199     {
200       ServerContext::DicomCacheLocker locker(GetContext(), instance);
201       ParsedDicomFile& original = locker.GetDicom();
202 
203       originalHasher.reset(new DicomInstanceHasher(original.GetHasher()));
204       modified.reset(original.Clone(true));
205     }
206     catch (OrthancException& e)
207     {
208       LOG(WARNING) << "An error occurred while executing a Modification job on instance " << instance << ": " << e.GetDetails();
209       return false;
210     }
211 
212 
213     /**
214      * Compute the resulting DICOM instance.
215      **/
216 
217     modification_->Apply(*modified);
218 
219     const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject());
220 
221     if (transcode_)
222     {
223       std::set<DicomTransferSyntax> syntaxes;
224       syntaxes.insert(transferSyntax_);
225 
226       IDicomTranscoder::DicomImage source;
227       source.AcquireParsed(*modified);  // "modified" is invalid below this point
228 
229       IDicomTranscoder::DicomImage transcoded;
230       if (GetContext().Transcode(transcoded, source, syntaxes, true))
231       {
232         modified.reset(transcoded.ReleaseAsParsedDicomFile());
233 
234         // Fix the SOP instance UID in order the preserve the
235         // references between instance UIDs in the DICOM hierarchy
236         // (the UID might have changed in the case of lossy transcoding)
237         if (modified.get() == NULL ||
238             modified->GetDcmtkObject().getDataset() == NULL ||
239             !modified->GetDcmtkObject().getDataset()->putAndInsertString(
240               DCM_SOPInstanceUID, modifiedUid.c_str(), OFTrue /* replace */).good())
241         {
242           throw OrthancException(ErrorCode_InternalError);
243         }
244       }
245       else
246       {
247         LOG(WARNING) << "Cannot transcode instance, keeping original transfer syntax: " << instance;
248         modified.reset(source.ReleaseAsParsedDicomFile());
249       }
250     }
251 
252     assert(modifiedUid == IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject()));
253 
254     std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*modified));
255     toStore->SetOrigin(origin_);
256 
257 
258     /**
259      * Prepare the metadata information to associate with the
260      * resulting DICOM instance (AnonymizedFrom/ModifiedFrom).
261      **/
262 
263     DicomInstanceHasher modifiedHasher = modified->GetHasher();
264 
265     MetadataType metadataType = (isAnonymization_ ?
266                                  MetadataType_AnonymizedFrom :
267                                  MetadataType_ModifiedFrom);
268 
269     if (originalHasher->HashSeries() != modifiedHasher.HashSeries())
270     {
271       toStore->AddMetadata(ResourceType_Series, metadataType, originalHasher->HashSeries());
272     }
273 
274     if (originalHasher->HashStudy() != modifiedHasher.HashStudy())
275     {
276       toStore->AddMetadata(ResourceType_Study, metadataType, originalHasher->HashStudy());
277     }
278 
279     if (originalHasher->HashPatient() != modifiedHasher.HashPatient())
280     {
281       toStore->AddMetadata(ResourceType_Patient, metadataType, originalHasher->HashPatient());
282     }
283 
284     assert(instance == originalHasher->HashInstance());
285     toStore->AddMetadata(ResourceType_Instance, metadataType, instance);
286 
287 
288     /**
289      * Store the resulting DICOM instance into the Orthanc store.
290      **/
291 
292     std::string modifiedInstance;
293     if (GetContext().Store(modifiedInstance, *toStore,
294                            StoreInstanceMode_Default) != StoreStatus_Success)
295     {
296       throw OrthancException(ErrorCode_CannotStoreInstance,
297                              "Error while storing a modified instance " + instance);
298     }
299 
300     /**
301      * The assertion below will fail if automated transcoding to a
302      * lossy transfer syntax is enabled in the Orthanc core, and if
303      * the source instance is not in this transfer syntax.
304      **/
305     // assert(modifiedInstance == modifiedHasher.HashInstance());
306 
307     output_->Update(modifiedHasher);
308 
309     return true;
310   }
311 
312 
ResourceModificationJob(ServerContext & context)313   ResourceModificationJob::ResourceModificationJob(ServerContext& context) :
314     CleaningInstancesJob(context, true /* by default, keep source */),
315     isAnonymization_(false),
316     transcode_(false),
317     transferSyntax_(DicomTransferSyntax_LittleEndianExplicit)  // dummy initialization
318   {
319   }
320 
321 
SetSingleResourceModification(DicomModification * modification,ResourceType outputLevel,bool isAnonymization)322   void ResourceModificationJob::SetSingleResourceModification(DicomModification* modification,
323                                                               ResourceType outputLevel,
324                                                               bool isAnonymization)
325   {
326     if (modification == NULL)
327     {
328       throw OrthancException(ErrorCode_NullPointer);
329     }
330     else if (IsStarted())
331     {
332       throw OrthancException(ErrorCode_BadSequenceOfCalls);
333     }
334     else
335     {
336       modification_.reset(modification);
337       output_.reset(new SingleOutput(outputLevel));
338       isAnonymization_ = isAnonymization;
339     }
340   }
341 
342 
SetMultipleResourcesModification(DicomModification * modification,bool isAnonymization)343   void ResourceModificationJob::SetMultipleResourcesModification(DicomModification* modification,
344                                                                  bool isAnonymization)
345   {
346     if (modification == NULL)
347     {
348       throw OrthancException(ErrorCode_NullPointer);
349     }
350     else if (IsStarted())
351     {
352       throw OrthancException(ErrorCode_BadSequenceOfCalls);
353     }
354     else
355     {
356       modification_.reset(modification);
357       output_.reset(new MultipleOutputs);
358       isAnonymization_ = isAnonymization;
359     }
360   }
361 
362 
SetOrigin(const DicomInstanceOrigin & origin)363   void ResourceModificationJob::SetOrigin(const DicomInstanceOrigin& origin)
364   {
365     if (IsStarted())
366     {
367       throw OrthancException(ErrorCode_BadSequenceOfCalls);
368     }
369     else
370     {
371       origin_ = origin;
372     }
373   }
374 
375 
SetOrigin(const RestApiCall & call)376   void ResourceModificationJob::SetOrigin(const RestApiCall& call)
377   {
378     SetOrigin(DicomInstanceOrigin::FromRest(call));
379   }
380 
381 
GetModification() const382   const DicomModification& ResourceModificationJob::GetModification() const
383   {
384     if (modification_.get() == NULL)
385     {
386       throw OrthancException(ErrorCode_BadSequenceOfCalls);
387     }
388     else
389     {
390       return *modification_;
391     }
392   }
393 
394 
GetTransferSyntax() const395   DicomTransferSyntax ResourceModificationJob::GetTransferSyntax() const
396   {
397     if (transcode_)
398     {
399       return transferSyntax_;
400     }
401     else
402     {
403       throw OrthancException(ErrorCode_BadSequenceOfCalls);
404     }
405   }
406 
407 
SetTranscode(DicomTransferSyntax syntax)408   void ResourceModificationJob::SetTranscode(DicomTransferSyntax syntax)
409   {
410     if (IsStarted())
411     {
412       throw OrthancException(ErrorCode_BadSequenceOfCalls);
413     }
414     else
415     {
416       transcode_ = true;
417       transferSyntax_ = syntax;
418     }
419   }
420 
421 
SetTranscode(const std::string & transferSyntaxUid)422   void ResourceModificationJob::SetTranscode(const std::string& transferSyntaxUid)
423   {
424     DicomTransferSyntax s;
425     if (LookupTransferSyntax(s, transferSyntaxUid))
426     {
427       SetTranscode(s);
428     }
429     else
430     {
431       throw OrthancException(ErrorCode_BadFileFormat,
432                              "Unknown transfer syntax UID: " + transferSyntaxUid);
433     }
434   }
435 
436 
ClearTranscode()437   void ResourceModificationJob::ClearTranscode()
438   {
439     if (IsStarted())
440     {
441       throw OrthancException(ErrorCode_BadSequenceOfCalls);
442     }
443     else
444     {
445       transcode_ = false;
446     }
447   }
448 
449 
IsSingleResourceModification() const450   bool ResourceModificationJob::IsSingleResourceModification() const
451   {
452     if (modification_.get() == NULL)
453     {
454       assert(output_.get() == NULL);
455       throw OrthancException(ErrorCode_BadSequenceOfCalls);
456     }
457     else
458     {
459       assert(output_.get() != NULL);
460       return output_->IsSingleResource();
461     }
462   }
463 
464 
GetOutputLevel() const465   ResourceType ResourceModificationJob::GetOutputLevel() const
466   {
467     if (IsSingleResourceModification())
468     {
469       assert(modification_.get() != NULL &&
470              output_.get() != NULL);
471       return dynamic_cast<const SingleOutput&>(*output_).GetLevel();
472     }
473     else
474     {
475       // Not applicable if multiple resources
476       throw OrthancException(ErrorCode_BadSequenceOfCalls);
477     }
478   }
479 
480 
GetPublicContent(Json::Value & value)481   void ResourceModificationJob::GetPublicContent(Json::Value& value)
482   {
483     CleaningInstancesJob::GetPublicContent(value);
484 
485     value["IsAnonymization"] = isAnonymization_;
486 
487     if (output_.get() != NULL)
488     {
489       output_->Format(value);
490     }
491 
492     if (transcode_)
493     {
494       value["Transcode"] = GetTransferSyntaxUid(transferSyntax_);
495     }
496   }
497 
498 
499   static const char* MODIFICATION = "Modification";
500   static const char* ORIGIN = "Origin";
501   static const char* IS_ANONYMIZATION = "IsAnonymization";
502   static const char* TRANSCODE = "Transcode";
503   static const char* OUTPUT_LEVEL = "OutputLevel";
504   static const char* IS_SINGLE_RESOURCE = "IsSingleResource";
505 
506 
ResourceModificationJob(ServerContext & context,const Json::Value & serialized)507   ResourceModificationJob::ResourceModificationJob(ServerContext& context,
508                                                    const Json::Value& serialized) :
509     CleaningInstancesJob(context, serialized, true /* by default, keep source */),
510     transferSyntax_(DicomTransferSyntax_LittleEndianExplicit)  // dummy initialization
511   {
512     assert(serialized.type() == Json::objectValue);
513 
514     origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
515 
516     if (serialized.isMember(TRANSCODE))
517     {
518       SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE));
519     }
520     else
521     {
522       transcode_ = false;
523     }
524 
525     bool isSingleResource;
526     if (serialized.isMember(IS_SINGLE_RESOURCE))
527     {
528       isSingleResource = SerializationToolbox::ReadBoolean(serialized, IS_SINGLE_RESOURCE);
529     }
530     else
531     {
532       isSingleResource = true;  // Backward compatibility with Orthanc <= 1.9.3
533     }
534 
535     bool isAnonymization = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
536     std::unique_ptr<DicomModification> modification(new DicomModification(serialized[MODIFICATION]));
537 
538     if (isSingleResource)
539     {
540       ResourceType outputLevel;
541 
542       if (serialized.isMember(OUTPUT_LEVEL))
543       {
544         // New in Orthanc 1.9.4. This fixes an *incorrect* behavior in
545         // Orthanc <= 1.9.3, in which "outputLevel" would be set to
546         // "modification->GetLevel()"
547         outputLevel = StringToResourceType(SerializationToolbox::ReadString(serialized, OUTPUT_LEVEL).c_str());
548       }
549       else
550       {
551         // Use the buggy convention from Orthanc <= 1.9.3 (which is
552         // the only thing we have at hand)
553         outputLevel = modification->GetLevel();
554 
555         if (outputLevel == ResourceType_Instance)
556         {
557           // This should never happen, but as "SingleOutput" doesn't
558           // support instance-level anonymization, don't take any risk
559           // and choose an arbitrary output level
560           outputLevel = ResourceType_Patient;
561         }
562       }
563 
564       SetSingleResourceModification(modification.release(), outputLevel, isAnonymization);
565     }
566     else
567     {
568       // New in Orthanc 1.9.4
569       SetMultipleResourcesModification(modification.release(), isAnonymization);
570     }
571   }
572 
Serialize(Json::Value & value)573   bool ResourceModificationJob::Serialize(Json::Value& value)
574   {
575     if (modification_.get() == NULL)
576     {
577       throw OrthancException(ErrorCode_BadSequenceOfCalls);
578     }
579     else if (!CleaningInstancesJob::Serialize(value))
580     {
581       return false;
582     }
583     else
584     {
585       assert(value.type() == Json::objectValue);
586 
587       value[IS_ANONYMIZATION] = isAnonymization_;
588 
589       if (transcode_)
590       {
591         value[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
592       }
593 
594       origin_.Serialize(value[ORIGIN]);
595 
596       Json::Value tmp;
597       modification_->Serialize(tmp);
598       value[MODIFICATION] = tmp;
599 
600       // New in Orthanc 1.9.4
601       value[IS_SINGLE_RESOURCE] = IsSingleResourceModification();
602       if (IsSingleResourceModification())
603       {
604         value[OUTPUT_LEVEL] = EnumerationToString(GetOutputLevel());
605       }
606 
607       return true;
608     }
609   }
610 }
611