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