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 "ServerContext.h" 36 37 #include "../../OrthancFramework/Sources/Cache/SharedArchive.h" 38 #include "../../OrthancFramework/Sources/DicomFormat/DicomElement.h" 39 #include "../../OrthancFramework/Sources/DicomFormat/DicomStreamReader.h" 40 #include "../../OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h" 41 #include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h" 42 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" 43 #include "../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" 44 #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" 45 #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h" 46 #include "../../OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h" 47 #include "../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h" 48 #include "../../OrthancFramework/Sources/Logging.h" 49 #include "../../OrthancFramework/Sources/MetricsRegistry.h" 50 #include "../Plugins/Engine/OrthancPlugins.h" 51 52 #include "OrthancConfiguration.h" 53 #include "OrthancRestApi/OrthancRestApi.h" 54 #include "Search/DatabaseLookup.h" 55 #include "ServerJobs/OrthancJobUnserializer.h" 56 #include "ServerToolbox.h" 57 #include "StorageCommitmentReports.h" 58 59 #include <dcmtk/dcmdata/dcfilefo.h> 60 61 62 static size_t DICOM_CACHE_SIZE = 128 * 1024 * 1024; // 128 MB 63 64 65 /** 66 * IMPORTANT: We make the assumption that the same instance of 67 * FileStorage can be accessed from multiple threads. This seems OK 68 * since the filesystem implements the required locking mechanisms, 69 * but maybe a read-writer lock on the "FileStorage" could be 70 * useful. Conversely, "ServerIndex" already implements mutex-based 71 * locking. 72 **/ 73 74 namespace Orthanc 75 { IsUncompressedTransferSyntax(DicomTransferSyntax transferSyntax)76 static bool IsUncompressedTransferSyntax(DicomTransferSyntax transferSyntax) 77 { 78 return (transferSyntax == DicomTransferSyntax_LittleEndianImplicit || 79 transferSyntax == DicomTransferSyntax_LittleEndianExplicit || 80 transferSyntax == DicomTransferSyntax_BigEndianExplicit); 81 } 82 83 IsTranscodableTransferSyntax(DicomTransferSyntax transferSyntax)84 static bool IsTranscodableTransferSyntax(DicomTransferSyntax transferSyntax) 85 { 86 return ( 87 // Do not try to transcode DICOM videos (new in Orthanc 1.8.2) 88 transferSyntax != DicomTransferSyntax_MPEG2MainProfileAtMainLevel && 89 transferSyntax != DicomTransferSyntax_MPEG2MainProfileAtHighLevel && 90 transferSyntax != DicomTransferSyntax_MPEG4HighProfileLevel4_1 && 91 transferSyntax != DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1 && 92 transferSyntax != DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo && 93 transferSyntax != DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo && 94 transferSyntax != DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2 && 95 transferSyntax != DicomTransferSyntax_HEVCMainProfileLevel5_1 && 96 transferSyntax != DicomTransferSyntax_HEVCMain10ProfileLevel5_1 && 97 98 // Do not try to transcode special transfer syntaxes 99 transferSyntax != DicomTransferSyntax_RFC2557MimeEncapsulation && 100 transferSyntax != DicomTransferSyntax_XML); 101 } 102 103 ChangeThread(ServerContext * that,unsigned int sleepDelay)104 void ServerContext::ChangeThread(ServerContext* that, 105 unsigned int sleepDelay) 106 { 107 while (!that->done_) 108 { 109 std::unique_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(sleepDelay)); 110 111 if (obj.get() != NULL) 112 { 113 const ServerIndexChange& change = dynamic_cast<const ServerIndexChange&>(*obj.get()); 114 115 boost::shared_lock<boost::shared_mutex> lock(that->listenersMutex_); 116 for (ServerListeners::iterator it = that->listeners_.begin(); 117 it != that->listeners_.end(); ++it) 118 { 119 try 120 { 121 try 122 { 123 it->GetListener().SignalChange(change); 124 } 125 catch (std::bad_alloc&) 126 { 127 LOG(ERROR) << "Not enough memory while signaling a change"; 128 } 129 catch (...) 130 { 131 throw OrthancException(ErrorCode_InternalError); 132 } 133 } 134 catch (OrthancException& e) 135 { 136 LOG(ERROR) << "Error in the " << it->GetDescription() 137 << " callback while signaling a change: " << e.What() 138 << " (code " << e.GetErrorCode() << ")"; 139 } 140 } 141 } 142 } 143 } 144 145 SaveJobsThread(ServerContext * that,unsigned int sleepDelay)146 void ServerContext::SaveJobsThread(ServerContext* that, 147 unsigned int sleepDelay) 148 { 149 static const boost::posix_time::time_duration PERIODICITY = 150 boost::posix_time::seconds(10); 151 152 boost::posix_time::ptime next = 153 boost::posix_time::microsec_clock::universal_time() + PERIODICITY; 154 155 while (!that->done_) 156 { 157 boost::this_thread::sleep(boost::posix_time::milliseconds(sleepDelay)); 158 159 if (that->haveJobsChanged_ || 160 boost::posix_time::microsec_clock::universal_time() >= next) 161 { 162 that->haveJobsChanged_ = false; 163 that->SaveJobsEngine(); 164 next = boost::posix_time::microsec_clock::universal_time() + PERIODICITY; 165 } 166 } 167 } 168 169 SignalJobSubmitted(const std::string & jobId)170 void ServerContext::SignalJobSubmitted(const std::string& jobId) 171 { 172 haveJobsChanged_ = true; 173 mainLua_.SignalJobSubmitted(jobId); 174 175 #if ORTHANC_ENABLE_PLUGINS == 1 176 if (HasPlugins()) 177 { 178 GetPlugins().SignalJobSubmitted(jobId); 179 } 180 #endif 181 } 182 183 SignalJobSuccess(const std::string & jobId)184 void ServerContext::SignalJobSuccess(const std::string& jobId) 185 { 186 haveJobsChanged_ = true; 187 mainLua_.SignalJobSuccess(jobId); 188 189 #if ORTHANC_ENABLE_PLUGINS == 1 190 if (HasPlugins()) 191 { 192 GetPlugins().SignalJobSuccess(jobId); 193 } 194 #endif 195 } 196 197 SignalJobFailure(const std::string & jobId)198 void ServerContext::SignalJobFailure(const std::string& jobId) 199 { 200 haveJobsChanged_ = true; 201 mainLua_.SignalJobFailure(jobId); 202 203 #if ORTHANC_ENABLE_PLUGINS == 1 204 if (HasPlugins()) 205 { 206 GetPlugins().SignalJobFailure(jobId); 207 } 208 #endif 209 } 210 211 SetupJobsEngine(bool unitTesting,bool loadJobsFromDatabase)212 void ServerContext::SetupJobsEngine(bool unitTesting, 213 bool loadJobsFromDatabase) 214 { 215 if (loadJobsFromDatabase) 216 { 217 std::string serialized; 218 if (index_.LookupGlobalProperty(serialized, GlobalProperty_JobsRegistry, false /* not shared */)) 219 { 220 LOG(WARNING) << "Reloading the jobs from the last execution of Orthanc"; 221 222 try 223 { 224 OrthancJobUnserializer unserializer(*this); 225 jobsEngine_.LoadRegistryFromString(unserializer, serialized); 226 } 227 catch (OrthancException& e) 228 { 229 LOG(WARNING) << "Cannot unserialize the jobs engine, starting anyway: " << e.What(); 230 } 231 } 232 else 233 { 234 LOG(INFO) << "The last execution of Orthanc has archived no job"; 235 } 236 } 237 else 238 { 239 LOG(INFO) << "Not reloading the jobs from the last execution of Orthanc"; 240 } 241 242 jobsEngine_.GetRegistry().SetObserver(*this); 243 jobsEngine_.Start(); 244 isJobsEngineUnserialized_ = true; 245 246 saveJobsThread_ = boost::thread(SaveJobsThread, this, (unitTesting ? 20 : 100)); 247 } 248 249 SaveJobsEngine()250 void ServerContext::SaveJobsEngine() 251 { 252 if (saveJobs_) 253 { 254 LOG(TRACE) << "Serializing the content of the jobs engine"; 255 256 try 257 { 258 Json::Value value; 259 jobsEngine_.GetRegistry().Serialize(value); 260 261 std::string serialized; 262 Toolbox::WriteFastJson(serialized, value); 263 264 index_.SetGlobalProperty(GlobalProperty_JobsRegistry, false /* not shared */, serialized); 265 } 266 catch (OrthancException& e) 267 { 268 LOG(ERROR) << "Cannot serialize the jobs engine: " << e.What(); 269 } 270 } 271 } 272 273 PublishDicomCacheMetrics()274 void ServerContext::PublishDicomCacheMetrics() 275 { 276 metricsRegistry_->SetValue("orthanc_dicom_cache_size", 277 static_cast<float>(dicomCache_.GetCurrentSize()) / static_cast<float>(1024 * 1024)); 278 metricsRegistry_->SetValue("orthanc_dicom_cache_count", 279 static_cast<float>(dicomCache_.GetNumberOfItems())); 280 } 281 282 ServerContext(IDatabaseWrapper & database,IStorageArea & area,bool unitTesting,size_t maxCompletedJobs)283 ServerContext::ServerContext(IDatabaseWrapper& database, 284 IStorageArea& area, 285 bool unitTesting, 286 size_t maxCompletedJobs) : 287 index_(*this, database, (unitTesting ? 20 : 500)), 288 area_(area), 289 compressionEnabled_(false), 290 storeMD5_(true), 291 largeDicomThrottler_(1), 292 dicomCache_(DICOM_CACHE_SIZE), 293 mainLua_(*this), 294 filterLua_(*this), 295 luaListener_(*this), 296 jobsEngine_(maxCompletedJobs), 297 #if ORTHANC_ENABLE_PLUGINS == 1 298 plugins_(NULL), 299 #endif 300 done_(false), 301 haveJobsChanged_(false), 302 isJobsEngineUnserialized_(false), 303 metricsRegistry_(new MetricsRegistry), 304 isHttpServerSecure_(true), 305 isExecuteLuaEnabled_(false), 306 overwriteInstances_(false), 307 dcmtkTranscoder_(new DcmtkTranscoder), 308 isIngestTranscoding_(false), 309 ingestTranscodingOfUncompressed_(true), 310 ingestTranscodingOfCompressed_(true), 311 preferredTransferSyntax_(DicomTransferSyntax_LittleEndianExplicit), 312 deidentifyLogs_(false) 313 { 314 try 315 { 316 unsigned int lossyQuality; 317 318 { 319 OrthancConfiguration::ReaderLock lock; 320 321 queryRetrieveArchive_.reset( 322 new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100))); 323 mediaArchive_.reset( 324 new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1))); 325 defaultLocalAet_ = lock.GetConfiguration().GetOrthancAET(); 326 jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2)); 327 saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true); 328 metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true)); 329 330 // New configuration options in Orthanc 1.5.1 331 findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always")); 332 limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0); 333 limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0); 334 335 // New configuration option in Orthanc 1.6.0 336 storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100))); 337 338 // New options in Orthanc 1.7.0 339 transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true); 340 builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder", "After")); 341 lossyQuality = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomLossyTranscodingQuality", 90); 342 343 std::string s; 344 if (lock.GetConfiguration().LookupStringParameter(s, "IngestTranscoding")) 345 { 346 if (LookupTransferSyntax(ingestTransferSyntax_, s)) 347 { 348 isIngestTranscoding_ = true; 349 LOG(WARNING) << "Incoming DICOM instances will automatically be transcoded to " 350 << "transfer syntax: " << GetTransferSyntaxUid(ingestTransferSyntax_); 351 352 // New options in Orthanc 1.8.2 353 ingestTranscodingOfUncompressed_ = lock.GetConfiguration().GetBooleanParameter("IngestTranscodingOfUncompressed", true); 354 ingestTranscodingOfCompressed_ = lock.GetConfiguration().GetBooleanParameter("IngestTranscodingOfCompressed", true); 355 356 LOG(WARNING) << " Ingest transcoding will " 357 << (ingestTranscodingOfUncompressed_ ? "be applied" : "*not* be applied") 358 << " to uncompressed transfer syntaxes (Little Endian Implicit/Explicit, Big Endian Explicit)"; 359 360 LOG(WARNING) << " Ingest transcoding will " 361 << (ingestTranscodingOfCompressed_ ? "be applied" : "*not* be applied") 362 << " to compressed transfer syntaxes"; 363 } 364 else 365 { 366 throw OrthancException(ErrorCode_ParameterOutOfRange, 367 "Unknown transfer syntax for ingest transcoding: " + s); 368 } 369 } 370 else 371 { 372 isIngestTranscoding_ = false; 373 LOG(INFO) << "Automated transcoding of incoming DICOM instances is disabled"; 374 } 375 376 // New options in Orthanc 1.8.2 377 if (lock.GetConfiguration().GetBooleanParameter("DeidentifyLogs", true)) 378 { 379 deidentifyLogs_ = true; 380 CLOG(INFO, DICOM) << "Deidentification of log contents (notably for DIMSE queries) is enabled"; 381 382 DicomVersion version = StringToDicomVersion( 383 lock.GetConfiguration().GetStringParameter("DeidentifyLogsDicomVersion", "2017c")); 384 CLOG(INFO, DICOM) << "Version of DICOM standard used for deidentification is " 385 << EnumerationToString(version); 386 387 logsDeidentifierRules_.SetupAnonymization(version); 388 } 389 else 390 { 391 deidentifyLogs_ = false; 392 CLOG(INFO, DICOM) << "Deidentification of log contents (notably for DIMSE queries) is disabled"; 393 } 394 395 // New options in Orthanc 1.9.0 396 if (lock.GetConfiguration().LookupStringParameter(s, "DicomScuPreferredTransferSyntax") && 397 !LookupTransferSyntax(preferredTransferSyntax_, s)) 398 { 399 throw OrthancException(ErrorCode_ParameterOutOfRange, 400 "Unknown preferred transfer syntax: " + s); 401 } 402 403 CLOG(INFO, DICOM) << "Preferred transfer syntax for Orthanc C-STORE SCU: " 404 << GetTransferSyntaxUid(preferredTransferSyntax_); 405 406 lock.GetConfiguration().GetAcceptedTransferSyntaxes(acceptedTransferSyntaxes_); 407 408 isUnknownSopClassAccepted_ = lock.GetConfiguration().GetBooleanParameter("UnknownSopClassAccepted", false); 409 } 410 411 jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); 412 413 listeners_.push_back(ServerListener(luaListener_, "Lua")); 414 changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100)); 415 416 dynamic_cast<DcmtkTranscoder&>(*dcmtkTranscoder_).SetLossyQuality(lossyQuality); 417 } 418 catch (OrthancException&) 419 { 420 Stop(); 421 throw; 422 } 423 } 424 425 426 ~ServerContext()427 ServerContext::~ServerContext() 428 { 429 if (!done_) 430 { 431 LOG(ERROR) << "INTERNAL ERROR: ServerContext::Stop() should be invoked manually to avoid mess in the destruction order!"; 432 Stop(); 433 } 434 } 435 436 Stop()437 void ServerContext::Stop() 438 { 439 if (!done_) 440 { 441 { 442 boost::unique_lock<boost::shared_mutex> lock(listenersMutex_); 443 listeners_.clear(); 444 } 445 446 done_ = true; 447 448 if (changeThread_.joinable()) 449 { 450 changeThread_.join(); 451 } 452 453 if (saveJobsThread_.joinable()) 454 { 455 saveJobsThread_.join(); 456 } 457 458 jobsEngine_.GetRegistry().ResetObserver(); 459 460 if (isJobsEngineUnserialized_) 461 { 462 // Avoid losing jobs if the JobsRegistry cannot be unserialized 463 SaveJobsEngine(); 464 } 465 466 // Do not change the order below! 467 jobsEngine_.Stop(); 468 index_.Stop(); 469 } 470 } 471 472 SetCompressionEnabled(bool enabled)473 void ServerContext::SetCompressionEnabled(bool enabled) 474 { 475 if (enabled) 476 LOG(WARNING) << "Disk compression is enabled"; 477 else 478 LOG(WARNING) << "Disk compression is disabled"; 479 480 compressionEnabled_ = enabled; 481 } 482 483 RemoveFile(const std::string & fileUuid,FileContentType type)484 void ServerContext::RemoveFile(const std::string& fileUuid, 485 FileContentType type) 486 { 487 StorageAccessor accessor(area_, GetMetricsRegistry()); 488 accessor.Remove(fileUuid, type); 489 } 490 491 StoreAfterTranscoding(std::string & resultPublicId,DicomInstanceToStore & dicom,StoreInstanceMode mode)492 StoreStatus ServerContext::StoreAfterTranscoding(std::string& resultPublicId, 493 DicomInstanceToStore& dicom, 494 StoreInstanceMode mode) 495 { 496 bool overwrite; 497 switch (mode) 498 { 499 case StoreInstanceMode_Default: 500 overwrite = overwriteInstances_; 501 break; 502 503 case StoreInstanceMode_OverwriteDuplicate: 504 overwrite = true; 505 break; 506 507 case StoreInstanceMode_IgnoreDuplicate: 508 overwrite = false; 509 break; 510 511 default: 512 throw OrthancException(ErrorCode_ParameterOutOfRange); 513 } 514 515 bool hasPixelDataOffset; 516 uint64_t pixelDataOffset; 517 hasPixelDataOffset = DicomStreamReader::LookupPixelDataOffset( 518 pixelDataOffset, dicom.GetBufferData(), dicom.GetBufferSize()); 519 520 DicomTransferSyntax transferSyntax; 521 bool hasTransferSyntax = dicom.LookupTransferSyntax(transferSyntax); 522 523 DicomMap summary; 524 dicom.GetSummary(summary); 525 526 try 527 { 528 MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms"); 529 StorageAccessor accessor(area_, GetMetricsRegistry()); 530 531 DicomInstanceHasher hasher(summary); 532 resultPublicId = hasher.HashInstance(); 533 534 Json::Value dicomAsJson; 535 dicom.GetDicomAsJson(dicomAsJson); 536 537 Json::Value simplifiedTags; 538 Toolbox::SimplifyDicomAsJson(simplifiedTags, dicomAsJson, DicomToJsonFormat_Human); 539 540 // Test if the instance must be filtered out 541 bool accepted = true; 542 543 { 544 boost::shared_lock<boost::shared_mutex> lock(listenersMutex_); 545 546 for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it) 547 { 548 try 549 { 550 if (!it->GetListener().FilterIncomingInstance(dicom, simplifiedTags)) 551 { 552 accepted = false; 553 break; 554 } 555 } 556 catch (OrthancException& e) 557 { 558 LOG(ERROR) << "Error in the " << it->GetDescription() 559 << " callback while receiving an instance: " << e.What() 560 << " (code " << e.GetErrorCode() << ")"; 561 throw; 562 } 563 } 564 } 565 566 if (!accepted) 567 { 568 LOG(INFO) << "An incoming instance has been discarded by the filter"; 569 return StoreStatus_FilteredOut; 570 } 571 572 // Remove the file from the DicomCache (useful if 573 // "OverwriteInstances" is set to "true") 574 dicomCache_.Invalidate(resultPublicId); 575 PublishDicomCacheMetrics(); 576 577 // TODO Should we use "gzip" instead? 578 CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); 579 580 FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), 581 FileContentType_Dicom, compression, storeMD5_); 582 583 ServerIndex::Attachments attachments; 584 attachments.push_back(dicomInfo); 585 586 FileInfo dicomUntilPixelData; 587 if (hasPixelDataOffset && 588 (!area_.HasReadRange() || 589 compressionEnabled_)) 590 { 591 dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, 592 FileContentType_DicomUntilPixelData, compression, storeMD5_); 593 attachments.push_back(dicomUntilPixelData); 594 } 595 596 typedef std::map<MetadataType, std::string> InstanceMetadata; 597 InstanceMetadata instanceMetadata; 598 StoreStatus status = index_.Store( 599 instanceMetadata, summary, attachments, dicom.GetMetadata(), dicom.GetOrigin(), overwrite, 600 hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset); 601 602 // Only keep the metadata for the "instance" level 603 dicom.ClearMetadata(); 604 605 for (InstanceMetadata::const_iterator it = instanceMetadata.begin(); 606 it != instanceMetadata.end(); ++it) 607 { 608 dicom.AddMetadata(ResourceType_Instance, it->first, it->second); 609 } 610 611 if (status != StoreStatus_Success) 612 { 613 accessor.Remove(dicomInfo); 614 615 if (dicomUntilPixelData.IsValid()) 616 { 617 accessor.Remove(dicomUntilPixelData); 618 } 619 } 620 621 switch (status) 622 { 623 case StoreStatus_Success: 624 LOG(INFO) << "New instance stored"; 625 break; 626 627 case StoreStatus_AlreadyStored: 628 LOG(INFO) << "Already stored"; 629 break; 630 631 case StoreStatus_Failure: 632 LOG(ERROR) << "Store failure"; 633 break; 634 635 default: 636 // This should never happen 637 break; 638 } 639 640 if (status == StoreStatus_Success || 641 status == StoreStatus_AlreadyStored) 642 { 643 boost::shared_lock<boost::shared_mutex> lock(listenersMutex_); 644 645 for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it) 646 { 647 try 648 { 649 it->GetListener().SignalStoredInstance(resultPublicId, dicom, simplifiedTags); 650 } 651 catch (OrthancException& e) 652 { 653 LOG(ERROR) << "Error in the " << it->GetDescription() 654 << " callback while receiving an instance: " << e.What() 655 << " (code " << e.GetErrorCode() << ")"; 656 } 657 } 658 } 659 660 return status; 661 } 662 catch (OrthancException& e) 663 { 664 if (e.GetErrorCode() == ErrorCode_InexistentTag) 665 { 666 summary.LogMissingTagsForStore(); 667 } 668 669 throw; 670 } 671 } 672 673 Store(std::string & resultPublicId,DicomInstanceToStore & dicom,StoreInstanceMode mode)674 StoreStatus ServerContext::Store(std::string& resultPublicId, 675 DicomInstanceToStore& dicom, 676 StoreInstanceMode mode) 677 { 678 if (!isIngestTranscoding_) 679 { 680 // No automated transcoding. This was the only path in Orthanc <= 1.6.1. 681 return StoreAfterTranscoding(resultPublicId, dicom, mode); 682 } 683 else 684 { 685 // Automated transcoding of incoming DICOM instance 686 687 bool transcode = false; 688 689 DicomTransferSyntax sourceSyntax; 690 if (!dicom.LookupTransferSyntax(sourceSyntax) || 691 sourceSyntax == ingestTransferSyntax_) 692 { 693 // Don't transcode if the incoming DICOM is already in the proper transfer syntax 694 transcode = false; 695 } 696 else if (!IsTranscodableTransferSyntax(sourceSyntax)) 697 { 698 // Don't try to transcode video files, this is useless (new in 699 // Orthanc 1.8.2). This could be accepted in the future if 700 // video transcoding gets implemented. 701 transcode = false; 702 } 703 else if (IsUncompressedTransferSyntax(sourceSyntax)) 704 { 705 // This is an uncompressed transfer syntax (new in Orthanc 1.8.2) 706 transcode = ingestTranscodingOfUncompressed_; 707 } 708 else 709 { 710 // This is an compressed transfer syntax (new in Orthanc 1.8.2) 711 transcode = ingestTranscodingOfCompressed_; 712 } 713 714 if (!transcode) 715 { 716 // No transcoding 717 return StoreAfterTranscoding(resultPublicId, dicom, mode); 718 } 719 else 720 { 721 // Trancoding 722 std::set<DicomTransferSyntax> syntaxes; 723 syntaxes.insert(ingestTransferSyntax_); 724 725 IDicomTranscoder::DicomImage source; 726 source.SetExternalBuffer(dicom.GetBufferData(), dicom.GetBufferSize()); 727 728 IDicomTranscoder::DicomImage transcoded; 729 if (Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */)) 730 { 731 std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile()); 732 733 std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*tmp)); 734 toStore->SetOrigin(dicom.GetOrigin()); 735 736 StoreStatus ok = StoreAfterTranscoding(resultPublicId, *toStore, mode); 737 assert(resultPublicId == tmp->GetHasher().HashInstance()); 738 739 return ok; 740 } 741 else 742 { 743 // Cannot transcode => store the original file 744 return StoreAfterTranscoding(resultPublicId, dicom, mode); 745 } 746 } 747 } 748 } 749 750 AnswerAttachment(RestApiOutput & output,const std::string & resourceId,FileContentType content)751 void ServerContext::AnswerAttachment(RestApiOutput& output, 752 const std::string& resourceId, 753 FileContentType content) 754 { 755 FileInfo attachment; 756 int64_t revision; 757 if (!index_.LookupAttachment(attachment, revision, resourceId, content)) 758 { 759 throw OrthancException(ErrorCode_UnknownResource); 760 } 761 else 762 { 763 StorageAccessor accessor(area_, GetMetricsRegistry()); 764 accessor.AnswerFile(output, attachment, GetFileContentMime(content)); 765 } 766 } 767 768 ChangeAttachmentCompression(const std::string & resourceId,FileContentType attachmentType,CompressionType compression)769 void ServerContext::ChangeAttachmentCompression(const std::string& resourceId, 770 FileContentType attachmentType, 771 CompressionType compression) 772 { 773 LOG(INFO) << "Changing compression type for attachment " 774 << EnumerationToString(attachmentType) 775 << " of resource " << resourceId << " to " 776 << compression; 777 778 FileInfo attachment; 779 int64_t revision; 780 if (!index_.LookupAttachment(attachment, revision, resourceId, attachmentType)) 781 { 782 throw OrthancException(ErrorCode_UnknownResource); 783 } 784 785 if (attachment.GetCompressionType() == compression) 786 { 787 // Nothing to do 788 return; 789 } 790 791 std::string content; 792 793 StorageAccessor accessor(area_, GetMetricsRegistry()); 794 accessor.Read(content, attachment); 795 796 FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(), 797 content.size(), attachmentType, compression, storeMD5_); 798 799 try 800 { 801 int64_t newRevision; // ignored 802 StoreStatus status = index_.AddAttachment(newRevision, modified, resourceId, 803 true, revision, modified.GetUncompressedMD5()); 804 if (status != StoreStatus_Success) 805 { 806 accessor.Remove(modified); 807 throw OrthancException(ErrorCode_Database); 808 } 809 } 810 catch (OrthancException&) 811 { 812 accessor.Remove(modified); 813 throw; 814 } 815 } 816 817 InjectEmptyPixelData(Json::Value & dicomAsJson)818 static void InjectEmptyPixelData(Json::Value& dicomAsJson) 819 { 820 // This is for backward compatibility with Orthanc <= 1.9.0 821 Json::Value pixelData = Json::objectValue; 822 pixelData["Name"] = "PixelData"; 823 pixelData["Type"] = "Null"; 824 pixelData["Value"] = Json::nullValue; 825 826 dicomAsJson["7fe0,0010"] = pixelData; 827 } 828 829 ReadDicomAsJson(Json::Value & result,const std::string & instancePublicId,const std::set<DicomTag> & ignoreTagLength)830 void ServerContext::ReadDicomAsJson(Json::Value& result, 831 const std::string& instancePublicId, 832 const std::set<DicomTag>& ignoreTagLength) 833 { 834 /** 835 * CASE 1: The DICOM file, truncated at pixel data, is available 836 * as an attachment (it was created either because the storage 837 * area does not support range reads, or it "StorageCompression" 838 * is enabled). Simply return this attachment. 839 **/ 840 841 FileInfo attachment; 842 int64_t revision; // Ignored 843 844 if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData)) 845 { 846 std::string dicom; 847 848 { 849 StorageAccessor accessor(area_, GetMetricsRegistry()); 850 accessor.Read(dicom, attachment); 851 } 852 853 ParsedDicomFile parsed(dicom); 854 OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength); 855 InjectEmptyPixelData(result); 856 } 857 else 858 { 859 /** 860 * The truncated DICOM file is not stored as a standalone 861 * attachment. Lookup whether the pixel data offset has already 862 * been computed for this instance. 863 **/ 864 865 bool hasPixelDataOffset; 866 uint64_t pixelDataOffset = 0; // dummy initialization 867 868 { 869 std::string s; 870 if (index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance, 871 MetadataType_Instance_PixelDataOffset)) 872 { 873 hasPixelDataOffset = false; 874 875 if (!s.empty()) 876 { 877 try 878 { 879 pixelDataOffset = boost::lexical_cast<uint64_t>(s); 880 hasPixelDataOffset = true; 881 } 882 catch (boost::bad_lexical_cast&) 883 { 884 } 885 } 886 887 if (!hasPixelDataOffset) 888 { 889 LOG(ERROR) << "Metadata \"PixelDataOffset\" is corrupted for instance: " << instancePublicId; 890 } 891 } 892 else 893 { 894 // This instance was created by a version of Orthanc <= 1.9.0 895 hasPixelDataOffset = false; 896 } 897 } 898 899 900 if (hasPixelDataOffset && 901 area_.HasReadRange() && 902 index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom) && 903 attachment.GetCompressionType() == CompressionType_None) 904 { 905 /** 906 * CASE 2: The pixel data offset is known, AND that a range read 907 * can be used to retrieve the truncated DICOM file. Note that 908 * this case cannot be used if "StorageCompression" option is 909 * "true". 910 **/ 911 912 std::unique_ptr<IMemoryBuffer> dicom; 913 { 914 MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_storage_read_range_duration_ms"); 915 dicom.reset(area_.ReadRange(attachment.GetUuid(), FileContentType_Dicom, 0, pixelDataOffset)); 916 } 917 918 if (dicom.get() == NULL) 919 { 920 throw OrthancException(ErrorCode_InternalError); 921 } 922 else 923 { 924 assert(dicom->GetSize() == pixelDataOffset); 925 ParsedDicomFile parsed(dicom->GetData(), dicom->GetSize()); 926 OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength); 927 InjectEmptyPixelData(result); 928 } 929 } 930 else if (ignoreTagLength.empty() && 931 index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomAsJson)) 932 { 933 /** 934 * CASE 3: This instance was created using Orthanc <= 935 * 1.9.0. Reuse the old "DICOM-as-JSON" attachment if available. 936 * This is for backward compatibility: A call to 937 * "/tools/invalidate-tags" or to one flavors of 938 * "/.../.../reconstruct" will disable this case. 939 **/ 940 941 std::string dicomAsJson; 942 943 { 944 StorageAccessor accessor(area_, GetMetricsRegistry()); 945 accessor.Read(dicomAsJson, attachment); 946 } 947 948 if (!Toolbox::ReadJson(result, dicomAsJson)) 949 { 950 throw OrthancException(ErrorCode_CorruptedFile, 951 "Corrupted DICOM-as-JSON attachment of instance: " + instancePublicId); 952 } 953 } 954 else 955 { 956 /** 957 * CASE 4: Neither the truncated DICOM file is accessible, nor 958 * the DICOM-as-JSON summary. We have to retrieve the full DICOM 959 * file from the storage area. 960 **/ 961 962 std::string dicom; 963 ReadDicom(dicom, instancePublicId); 964 965 ParsedDicomFile parsed(dicom); 966 OrthancConfiguration::DefaultDicomDatasetToJson(result, parsed, ignoreTagLength); 967 968 if (!hasPixelDataOffset) 969 { 970 /** 971 * The pixel data offset was never computed for this 972 * instance, which indicates that it was created using 973 * Orthanc <= 1.9.0, or that calls to 974 * "LookupPixelDataOffset()" from earlier versions of 975 * Orthanc have failed. Try again this precomputation now 976 * for future calls. 977 **/ 978 if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, dicom) && 979 pixelDataOffset < dicom.size()) 980 { 981 index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset, 982 boost::lexical_cast<std::string>(pixelDataOffset)); 983 984 if (!area_.HasReadRange() || 985 compressionEnabled_) 986 { 987 int64_t newRevision; 988 AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData, 989 dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset, 990 false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); 991 } 992 } 993 } 994 } 995 } 996 } 997 998 ReadDicomAsJson(Json::Value & result,const std::string & instancePublicId)999 void ServerContext::ReadDicomAsJson(Json::Value& result, 1000 const std::string& instancePublicId) 1001 { 1002 std::set<DicomTag> ignoreTagLength; 1003 ReadDicomAsJson(result, instancePublicId, ignoreTagLength); 1004 } 1005 1006 ReadDicom(std::string & dicom,const std::string & instancePublicId)1007 void ServerContext::ReadDicom(std::string& dicom, 1008 const std::string& instancePublicId) 1009 { 1010 int64_t revision; 1011 ReadAttachment(dicom, revision, instancePublicId, FileContentType_Dicom, true /* uncompress */); 1012 } 1013 1014 ReadDicomUntilPixelData(std::string & dicom,const std::string & instancePublicId)1015 bool ServerContext::ReadDicomUntilPixelData(std::string& dicom, 1016 const std::string& instancePublicId) 1017 { 1018 if (!area_.HasReadRange()) 1019 { 1020 return false; 1021 } 1022 1023 FileInfo attachment; 1024 int64_t revision; // Ignored 1025 if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom)) 1026 { 1027 throw OrthancException(ErrorCode_InternalError, 1028 "Unable to read the DICOM file of instance " + instancePublicId); 1029 } 1030 1031 std::string s; 1032 1033 if (attachment.GetCompressionType() == CompressionType_None && 1034 index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance, 1035 MetadataType_Instance_PixelDataOffset) && 1036 !s.empty()) 1037 { 1038 try 1039 { 1040 uint64_t pixelDataOffset = boost::lexical_cast<uint64_t>(s); 1041 1042 std::unique_ptr<IMemoryBuffer> buffer( 1043 area_.ReadRange(attachment.GetUuid(), attachment.GetContentType(), 0, pixelDataOffset)); 1044 buffer->MoveToString(dicom); 1045 return true; // Success 1046 } 1047 catch (boost::bad_lexical_cast&) 1048 { 1049 LOG(ERROR) << "Metadata \"PixelDataOffset\" is corrupted for instance: " << instancePublicId; 1050 } 1051 } 1052 1053 return false; 1054 } 1055 1056 ReadAttachment(std::string & result,int64_t & revision,const std::string & instancePublicId,FileContentType content,bool uncompressIfNeeded)1057 void ServerContext::ReadAttachment(std::string& result, 1058 int64_t& revision, 1059 const std::string& instancePublicId, 1060 FileContentType content, 1061 bool uncompressIfNeeded) 1062 { 1063 FileInfo attachment; 1064 if (!index_.LookupAttachment(attachment, revision, instancePublicId, content)) 1065 { 1066 throw OrthancException(ErrorCode_InternalError, 1067 "Unable to read attachment " + EnumerationToString(content) + 1068 " of instance " + instancePublicId); 1069 } 1070 1071 assert(attachment.GetContentType() == content); 1072 1073 { 1074 StorageAccessor accessor(area_, GetMetricsRegistry()); 1075 1076 if (uncompressIfNeeded) 1077 { 1078 accessor.Read(result, attachment); 1079 } 1080 else 1081 { 1082 // Do not uncompress the content of the storage area, return the 1083 // raw data 1084 accessor.ReadRaw(result, attachment); 1085 } 1086 } 1087 } 1088 1089 DicomCacheLocker(ServerContext & context,const std::string & instancePublicId)1090 ServerContext::DicomCacheLocker::DicomCacheLocker(ServerContext& context, 1091 const std::string& instancePublicId) : 1092 context_(context), 1093 instancePublicId_(instancePublicId) 1094 { 1095 accessor_.reset(new ParsedDicomCache::Accessor(context_.dicomCache_, instancePublicId)); 1096 1097 if (!accessor_->IsValid()) 1098 { 1099 accessor_.reset(NULL); 1100 1101 // Throttle to avoid loading several large DICOM files simultaneously 1102 largeDicomLocker_.reset(new Semaphore::Locker(context.largeDicomThrottler_)); 1103 1104 std::string content; 1105 context_.ReadDicom(content, instancePublicId); 1106 1107 // Release the throttle if loading "small" DICOM files (under 1108 // 50MB, which is an arbitrary value) 1109 if (content.size() < 50 * 1024 * 1024) 1110 { 1111 largeDicomLocker_.reset(NULL); 1112 } 1113 1114 dicom_.reset(new ParsedDicomFile(content)); 1115 dicomSize_ = content.size(); 1116 } 1117 1118 assert(accessor_.get() != NULL || 1119 dicom_.get() != NULL); 1120 } 1121 1122 ~DicomCacheLocker()1123 ServerContext::DicomCacheLocker::~DicomCacheLocker() 1124 { 1125 if (dicom_.get() != NULL) 1126 { 1127 try 1128 { 1129 context_.dicomCache_.Acquire(instancePublicId_, dicom_.release(), dicomSize_); 1130 context_.PublishDicomCacheMetrics(); 1131 } 1132 catch (OrthancException&) 1133 { 1134 } 1135 } 1136 } 1137 1138 GetDicom() const1139 ParsedDicomFile& ServerContext::DicomCacheLocker::GetDicom() const 1140 { 1141 if (dicom_.get() != NULL) 1142 { 1143 return *dicom_; 1144 } 1145 else 1146 { 1147 assert(accessor_.get() != NULL); 1148 return accessor_->GetDicom(); 1149 } 1150 } 1151 1152 SetStoreMD5ForAttachments(bool storeMD5)1153 void ServerContext::SetStoreMD5ForAttachments(bool storeMD5) 1154 { 1155 LOG(INFO) << "Storing MD5 for attachments: " << (storeMD5 ? "yes" : "no"); 1156 storeMD5_ = storeMD5; 1157 } 1158 1159 AddAttachment(int64_t & newRevision,const std::string & resourceId,FileContentType attachmentType,const void * data,size_t size,bool hasOldRevision,int64_t oldRevision,const std::string & oldMD5)1160 bool ServerContext::AddAttachment(int64_t& newRevision, 1161 const std::string& resourceId, 1162 FileContentType attachmentType, 1163 const void* data, 1164 size_t size, 1165 bool hasOldRevision, 1166 int64_t oldRevision, 1167 const std::string& oldMD5) 1168 { 1169 LOG(INFO) << "Adding attachment " << EnumerationToString(attachmentType) << " to resource " << resourceId; 1170 1171 // TODO Should we use "gzip" instead? 1172 CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); 1173 1174 StorageAccessor accessor(area_, GetMetricsRegistry()); 1175 FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_); 1176 1177 StoreStatus status = index_.AddAttachment( 1178 newRevision, attachment, resourceId, hasOldRevision, oldRevision, oldMD5); 1179 if (status != StoreStatus_Success) 1180 { 1181 accessor.Remove(attachment); 1182 return false; 1183 } 1184 else 1185 { 1186 return true; 1187 } 1188 } 1189 1190 DeleteResource(Json::Value & target,const std::string & uuid,ResourceType expectedType)1191 bool ServerContext::DeleteResource(Json::Value& target, 1192 const std::string& uuid, 1193 ResourceType expectedType) 1194 { 1195 if (expectedType == ResourceType_Instance) 1196 { 1197 // remove the file from the DicomCache 1198 dicomCache_.Invalidate(uuid); 1199 PublishDicomCacheMetrics(); 1200 } 1201 1202 return index_.DeleteResource(target, uuid, expectedType); 1203 } 1204 1205 SignalChange(const ServerIndexChange & change)1206 void ServerContext::SignalChange(const ServerIndexChange& change) 1207 { 1208 if (change.GetResourceType() == ResourceType_Instance && 1209 change.GetChangeType() == ChangeType_Deleted) 1210 { 1211 dicomCache_.Invalidate(change.GetPublicId()); 1212 PublishDicomCacheMetrics(); 1213 } 1214 1215 pendingChanges_.Enqueue(change.Clone()); 1216 } 1217 1218 1219 #if ORTHANC_ENABLE_PLUGINS == 1 SetPlugins(OrthancPlugins & plugins)1220 void ServerContext::SetPlugins(OrthancPlugins& plugins) 1221 { 1222 boost::unique_lock<boost::shared_mutex> lock(listenersMutex_); 1223 1224 plugins_ = &plugins; 1225 1226 // TODO REFACTOR THIS 1227 listeners_.clear(); 1228 listeners_.push_back(ServerListener(luaListener_, "Lua")); 1229 listeners_.push_back(ServerListener(plugins, "plugin")); 1230 } 1231 1232 ResetPlugins()1233 void ServerContext::ResetPlugins() 1234 { 1235 boost::unique_lock<boost::shared_mutex> lock(listenersMutex_); 1236 1237 plugins_ = NULL; 1238 1239 // TODO REFACTOR THIS 1240 listeners_.clear(); 1241 listeners_.push_back(ServerListener(luaListener_, "Lua")); 1242 } 1243 1244 GetPlugins() const1245 const OrthancPlugins& ServerContext::GetPlugins() const 1246 { 1247 if (HasPlugins()) 1248 { 1249 return *plugins_; 1250 } 1251 else 1252 { 1253 throw OrthancException(ErrorCode_InternalError); 1254 } 1255 } 1256 GetPlugins()1257 OrthancPlugins& ServerContext::GetPlugins() 1258 { 1259 if (HasPlugins()) 1260 { 1261 return *plugins_; 1262 } 1263 else 1264 { 1265 throw OrthancException(ErrorCode_InternalError); 1266 } 1267 } 1268 1269 #endif 1270 1271 HasPlugins() const1272 bool ServerContext::HasPlugins() const 1273 { 1274 #if ORTHANC_ENABLE_PLUGINS == 1 1275 return (plugins_ != NULL); 1276 #else 1277 return false; 1278 #endif 1279 } 1280 1281 ApplyInternal(ILookupVisitor & visitor,const DatabaseLookup & lookup,ResourceType queryLevel,size_t since,size_t limit)1282 void ServerContext::ApplyInternal(ILookupVisitor& visitor, 1283 const DatabaseLookup& lookup, 1284 ResourceType queryLevel, 1285 size_t since, 1286 size_t limit) 1287 { 1288 unsigned int databaseLimit = (queryLevel == ResourceType_Instance ? 1289 limitFindInstances_ : limitFindResults_); 1290 1291 std::vector<std::string> resources, instances; 1292 1293 { 1294 const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); 1295 GetIndex().ApplyLookupResources(resources, &instances, lookup, queryLevel, lookupLimit); 1296 } 1297 1298 bool complete = (databaseLimit == 0 || 1299 resources.size() <= databaseLimit); 1300 1301 LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size(); 1302 1303 /** 1304 * "resources" contains the Orthanc ID of the resource at level 1305 * "queryLevel", "instances" contains one the Orthanc ID of one 1306 * sample instance from this resource. 1307 **/ 1308 assert(resources.size() == instances.size()); 1309 1310 size_t countResults = 0; 1311 size_t skipped = 0; 1312 1313 const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded(); 1314 1315 for (size_t i = 0; i < instances.size(); i++) 1316 { 1317 // Optimization in Orthanc 1.5.1 - Don't read the full JSON from 1318 // the disk if only "main DICOM tags" are to be returned 1319 1320 std::unique_ptr<Json::Value> dicomAsJson; 1321 1322 bool hasOnlyMainDicomTags; 1323 DicomMap dicom; 1324 1325 if (findStorageAccessMode_ == FindStorageAccessMode_DatabaseOnly || 1326 findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer || 1327 lookup.HasOnlyMainDicomTags()) 1328 { 1329 // Case (1): The main DICOM tags, as stored in the database, 1330 // are sufficient to look for match 1331 1332 DicomMap tmp; 1333 if (!GetIndex().GetAllMainDicomTags(tmp, instances[i])) 1334 { 1335 // The instance has been removed during the execution of the 1336 // lookup, ignore it 1337 continue; 1338 } 1339 1340 // New in Orthanc 1.6.0: Only keep the main DICOM tags at the 1341 // level of interest for the query 1342 switch (queryLevel) 1343 { 1344 // WARNING: Don't reorder cases below, and don't add "break" 1345 case ResourceType_Instance: 1346 dicom.MergeMainDicomTags(tmp, ResourceType_Instance); 1347 1348 case ResourceType_Series: 1349 dicom.MergeMainDicomTags(tmp, ResourceType_Series); 1350 1351 case ResourceType_Study: 1352 dicom.MergeMainDicomTags(tmp, ResourceType_Study); 1353 1354 case ResourceType_Patient: 1355 dicom.MergeMainDicomTags(tmp, ResourceType_Patient); 1356 break; 1357 1358 default: 1359 throw OrthancException(ErrorCode_InternalError); 1360 } 1361 1362 hasOnlyMainDicomTags = true; 1363 } 1364 else 1365 { 1366 // Case (2): Need to read the "DICOM-as-JSON" attachment from 1367 // the storage area 1368 dicomAsJson.reset(new Json::Value); 1369 ReadDicomAsJson(*dicomAsJson, instances[i]); 1370 1371 dicom.FromDicomAsJson(*dicomAsJson); 1372 1373 // This map contains the entire JSON, i.e. more than the main DICOM tags 1374 hasOnlyMainDicomTags = false; 1375 } 1376 1377 if (lookup.IsMatch(dicom)) 1378 { 1379 if (skipped < since) 1380 { 1381 skipped++; 1382 } 1383 else if (limit != 0 && 1384 countResults >= limit) 1385 { 1386 // Too many results, don't mark as complete 1387 complete = false; 1388 break; 1389 } 1390 else 1391 { 1392 if ((findStorageAccessMode_ == FindStorageAccessMode_DiskOnLookupAndAnswer || 1393 findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer) && 1394 dicomAsJson.get() == NULL && 1395 isDicomAsJsonNeeded) 1396 { 1397 dicomAsJson.reset(new Json::Value); 1398 ReadDicomAsJson(*dicomAsJson, instances[i]); 1399 } 1400 1401 if (hasOnlyMainDicomTags) 1402 { 1403 // This is Case (1): The variable "dicom" only contains the main DICOM tags 1404 visitor.Visit(resources[i], instances[i], dicom, dicomAsJson.get()); 1405 } 1406 else 1407 { 1408 // Remove the non-main DICOM tags from "dicom" if Case (2) 1409 // was used, for consistency with Case (1) 1410 1411 DicomMap mainDicomTags; 1412 mainDicomTags.ExtractMainDicomTags(dicom); 1413 visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get()); 1414 } 1415 1416 countResults ++; 1417 } 1418 } 1419 } 1420 1421 if (complete) 1422 { 1423 visitor.MarkAsComplete(); 1424 } 1425 1426 LOG(INFO) << "Number of matching resources: " << countResults; 1427 } 1428 1429 1430 1431 namespace 1432 { 1433 class ModalitiesInStudyVisitor : public ServerContext::ILookupVisitor 1434 { 1435 private: 1436 class Study : public boost::noncopyable 1437 { 1438 private: 1439 std::string orthancId_; 1440 std::string instanceId_; 1441 DicomMap mainDicomTags_; 1442 Json::Value dicomAsJson_; 1443 std::set<std::string> modalitiesInStudy_; 1444 1445 public: Study(const std::string & instanceId,const DicomMap & seriesTags)1446 Study(const std::string& instanceId, 1447 const DicomMap& seriesTags) : 1448 instanceId_(instanceId), 1449 dicomAsJson_(Json::nullValue) 1450 { 1451 { 1452 DicomMap tmp; 1453 tmp.Assign(seriesTags); 1454 tmp.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "dummy", false); 1455 DicomInstanceHasher hasher(tmp); 1456 orthancId_ = hasher.HashStudy(); 1457 } 1458 1459 mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Study); 1460 mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Patient); 1461 AddModality(seriesTags); 1462 } 1463 AddModality(const DicomMap & seriesTags)1464 void AddModality(const DicomMap& seriesTags) 1465 { 1466 std::string modality; 1467 if (seriesTags.LookupStringValue(modality, DICOM_TAG_MODALITY, false) && 1468 !modality.empty()) 1469 { 1470 modalitiesInStudy_.insert(modality); 1471 } 1472 } 1473 SetDicomAsJson(const Json::Value & dicomAsJson)1474 void SetDicomAsJson(const Json::Value& dicomAsJson) 1475 { 1476 dicomAsJson_ = dicomAsJson; 1477 } 1478 GetOrthancId() const1479 const std::string& GetOrthancId() const 1480 { 1481 return orthancId_; 1482 } 1483 GetInstanceId() const1484 const std::string& GetInstanceId() const 1485 { 1486 return instanceId_; 1487 } 1488 GetMainDicomTags() const1489 const DicomMap& GetMainDicomTags() const 1490 { 1491 return mainDicomTags_; 1492 } 1493 GetDicomAsJson() const1494 const Json::Value* GetDicomAsJson() const 1495 { 1496 if (dicomAsJson_.type() == Json::nullValue) 1497 { 1498 return NULL; 1499 } 1500 else 1501 { 1502 return &dicomAsJson_; 1503 } 1504 } 1505 }; 1506 1507 typedef std::map<std::string, Study*> Studies; 1508 1509 bool isDicomAsJsonNeeded_; 1510 bool complete_; 1511 Studies studies_; 1512 1513 public: ModalitiesInStudyVisitor(bool isDicomAsJsonNeeded)1514 explicit ModalitiesInStudyVisitor(bool isDicomAsJsonNeeded) : 1515 isDicomAsJsonNeeded_(isDicomAsJsonNeeded), 1516 complete_(false) 1517 { 1518 } 1519 ~ModalitiesInStudyVisitor()1520 ~ModalitiesInStudyVisitor() 1521 { 1522 for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it) 1523 { 1524 assert(it->second != NULL); 1525 delete it->second; 1526 } 1527 1528 studies_.clear(); 1529 } 1530 IsDicomAsJsonNeeded() const1531 virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE 1532 { 1533 return isDicomAsJsonNeeded_; 1534 } 1535 MarkAsComplete()1536 virtual void MarkAsComplete() ORTHANC_OVERRIDE 1537 { 1538 complete_ = true; 1539 } 1540 Visit(const std::string & publicId,const std::string & instanceId,const DicomMap & seriesTags,const Json::Value * dicomAsJson)1541 virtual void Visit(const std::string& publicId, 1542 const std::string& instanceId, 1543 const DicomMap& seriesTags, 1544 const Json::Value* dicomAsJson) ORTHANC_OVERRIDE 1545 { 1546 std::string studyInstanceUid; 1547 if (seriesTags.LookupStringValue(studyInstanceUid, DICOM_TAG_STUDY_INSTANCE_UID, false)) 1548 { 1549 Studies::iterator found = studies_.find(studyInstanceUid); 1550 if (found == studies_.end()) 1551 { 1552 // New study 1553 std::unique_ptr<Study> study(new Study(instanceId, seriesTags)); 1554 1555 if (dicomAsJson != NULL) 1556 { 1557 study->SetDicomAsJson(*dicomAsJson); 1558 } 1559 1560 studies_[studyInstanceUid] = study.release(); 1561 } 1562 else 1563 { 1564 // Already existing study 1565 found->second->AddModality(seriesTags); 1566 } 1567 } 1568 } 1569 Forward(ILookupVisitor & callerVisitor,size_t since,size_t limit) const1570 void Forward(ILookupVisitor& callerVisitor, 1571 size_t since, 1572 size_t limit) const 1573 { 1574 size_t index = 0; 1575 size_t countForwarded = 0; 1576 1577 for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it, index++) 1578 { 1579 if (limit == 0 || 1580 (index >= since && 1581 index < limit)) 1582 { 1583 assert(it->second != NULL); 1584 const Study& study = *it->second; 1585 1586 countForwarded++; 1587 callerVisitor.Visit(study.GetOrthancId(), study.GetInstanceId(), 1588 study.GetMainDicomTags(), study.GetDicomAsJson()); 1589 } 1590 } 1591 1592 if (countForwarded == studies_.size()) 1593 { 1594 callerVisitor.MarkAsComplete(); 1595 } 1596 } 1597 }; 1598 } 1599 1600 Apply(ILookupVisitor & visitor,const DatabaseLookup & lookup,ResourceType queryLevel,size_t since,size_t limit)1601 void ServerContext::Apply(ILookupVisitor& visitor, 1602 const DatabaseLookup& lookup, 1603 ResourceType queryLevel, 1604 size_t since, 1605 size_t limit) 1606 { 1607 if (queryLevel == ResourceType_Study && 1608 lookup.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) 1609 { 1610 // Convert the study-level query, into a series-level query, 1611 // where "ModalitiesInStudy" is replaced by "Modality" 1612 DatabaseLookup seriesLookup; 1613 1614 for (size_t i = 0; i < lookup.GetConstraintsCount(); i++) 1615 { 1616 const DicomTagConstraint& constraint = lookup.GetConstraint(i); 1617 if (constraint.GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) 1618 { 1619 if ((constraint.GetConstraintType() == ConstraintType_Equal && constraint.GetValue().empty()) || 1620 (constraint.GetConstraintType() == ConstraintType_List && constraint.GetValues().empty())) 1621 { 1622 // Ignore universal lookup on "ModalitiesInStudy" (0008,0061), 1623 // this should have been handled by the caller 1624 ApplyInternal(visitor, lookup, queryLevel, since, limit); 1625 return; 1626 } 1627 else 1628 { 1629 DicomTagConstraint modality(constraint); 1630 modality.SetTag(DICOM_TAG_MODALITY); 1631 seriesLookup.AddConstraint(modality); 1632 } 1633 } 1634 else 1635 { 1636 seriesLookup.AddConstraint(constraint); 1637 } 1638 } 1639 1640 ModalitiesInStudyVisitor seriesVisitor(visitor.IsDicomAsJsonNeeded()); 1641 ApplyInternal(seriesVisitor, seriesLookup, ResourceType_Series, 0, 0); 1642 seriesVisitor.Forward(visitor, since, limit); 1643 } 1644 else 1645 { 1646 ApplyInternal(visitor, lookup, queryLevel, since, limit); 1647 } 1648 } 1649 1650 LookupOrReconstructMetadata(std::string & target,const std::string & publicId,ResourceType level,MetadataType metadata)1651 bool ServerContext::LookupOrReconstructMetadata(std::string& target, 1652 const std::string& publicId, 1653 ResourceType level, 1654 MetadataType metadata) 1655 { 1656 // This is a backwards-compatibility function, that can 1657 // reconstruct metadata that were not generated by an older 1658 // release of Orthanc 1659 1660 if (metadata == MetadataType_Instance_SopClassUid || 1661 metadata == MetadataType_Instance_TransferSyntax) 1662 { 1663 int64_t revision; // Ignored 1664 if (index_.LookupMetadata(target, revision, publicId, level, metadata)) 1665 { 1666 return true; 1667 } 1668 else 1669 { 1670 // These metadata are mandatory in DICOM instances, and were 1671 // introduced in Orthanc 1.2.0. The fact that 1672 // "LookupMetadata()" has failed indicates that this database 1673 // comes from an older release of Orthanc. 1674 1675 DicomTag tag(0, 0); 1676 1677 switch (metadata) 1678 { 1679 case MetadataType_Instance_SopClassUid: 1680 tag = DICOM_TAG_SOP_CLASS_UID; 1681 break; 1682 1683 case MetadataType_Instance_TransferSyntax: 1684 tag = DICOM_TAG_TRANSFER_SYNTAX_UID; 1685 break; 1686 1687 default: 1688 throw OrthancException(ErrorCode_InternalError); 1689 } 1690 1691 Json::Value dicomAsJson; 1692 ReadDicomAsJson(dicomAsJson, publicId); 1693 1694 DicomMap tags; 1695 tags.FromDicomAsJson(dicomAsJson); 1696 1697 const DicomValue* value = tags.TestAndGetValue(tag); 1698 1699 if (value != NULL && 1700 !value->IsNull() && 1701 !value->IsBinary()) 1702 { 1703 target = value->GetContent(); 1704 1705 // Store for reuse 1706 index_.OverwriteMetadata(publicId, metadata, target); 1707 return true; 1708 } 1709 else 1710 { 1711 // Should never happen 1712 return false; 1713 } 1714 } 1715 } 1716 else 1717 { 1718 // No backward 1719 int64_t revision; // Ignored 1720 return index_.LookupMetadata(target, revision, publicId, level, metadata); 1721 } 1722 } 1723 1724 AddChildInstances(SetOfInstancesJob & job,const std::string & publicId)1725 void ServerContext::AddChildInstances(SetOfInstancesJob& job, 1726 const std::string& publicId) 1727 { 1728 std::list<std::string> instances; 1729 GetIndex().GetChildInstances(instances, publicId); 1730 1731 job.Reserve(job.GetInstancesCount() + instances.size()); 1732 1733 for (std::list<std::string>::const_iterator 1734 it = instances.begin(); it != instances.end(); ++it) 1735 { 1736 job.AddInstance(*it); 1737 } 1738 } 1739 1740 SignalUpdatedModalities()1741 void ServerContext::SignalUpdatedModalities() 1742 { 1743 #if ORTHANC_ENABLE_PLUGINS == 1 1744 if (HasPlugins()) 1745 { 1746 GetPlugins().SignalUpdatedModalities(); 1747 } 1748 #endif 1749 } 1750 1751 SignalUpdatedPeers()1752 void ServerContext::SignalUpdatedPeers() 1753 { 1754 #if ORTHANC_ENABLE_PLUGINS == 1 1755 if (HasPlugins()) 1756 { 1757 GetPlugins().SignalUpdatedPeers(); 1758 } 1759 #endif 1760 } 1761 1762 1763 IStorageCommitmentFactory::ILookupHandler* CreateStorageCommitment(const std::string & jobId,const std::string & transactionUid,const std::vector<std::string> & sopClassUids,const std::vector<std::string> & sopInstanceUids,const std::string & remoteAet,const std::string & calledAet)1764 ServerContext::CreateStorageCommitment(const std::string& jobId, 1765 const std::string& transactionUid, 1766 const std::vector<std::string>& sopClassUids, 1767 const std::vector<std::string>& sopInstanceUids, 1768 const std::string& remoteAet, 1769 const std::string& calledAet) 1770 { 1771 #if ORTHANC_ENABLE_PLUGINS == 1 1772 if (HasPlugins()) 1773 { 1774 return GetPlugins().CreateStorageCommitment( 1775 jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet); 1776 } 1777 #endif 1778 1779 return NULL; 1780 } 1781 1782 DecodeDicomFrame(const std::string & publicId,unsigned int frameIndex)1783 ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& publicId, 1784 unsigned int frameIndex) 1785 { 1786 if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) 1787 { 1788 // Use Orthanc's built-in decoder, using the cache to speed-up 1789 // things on multi-frame images 1790 1791 std::unique_ptr<ImageAccessor> decoded; 1792 try 1793 { 1794 ServerContext::DicomCacheLocker locker(*this, publicId); 1795 decoded.reset(locker.GetDicom().DecodeFrame(frameIndex)); 1796 } 1797 catch (OrthancException& e) 1798 { 1799 } 1800 1801 if (decoded.get() != NULL) 1802 { 1803 return decoded.release(); 1804 } 1805 } 1806 1807 #if ORTHANC_ENABLE_PLUGINS == 1 1808 if (HasPlugins() && 1809 GetPlugins().HasCustomImageDecoder()) 1810 { 1811 // TODO: Store the raw buffer in the DicomCacheLocker 1812 std::string dicomContent; 1813 ReadDicom(dicomContent, publicId); 1814 1815 std::unique_ptr<ImageAccessor> decoded; 1816 try 1817 { 1818 decoded.reset(GetPlugins().Decode(dicomContent.c_str(), dicomContent.size(), frameIndex)); 1819 } 1820 catch (OrthancException& e) 1821 { 1822 } 1823 1824 if (decoded.get() != NULL) 1825 { 1826 return decoded.release(); 1827 } 1828 else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) 1829 { 1830 LOG(INFO) << "The installed image decoding plugins cannot handle an image, " 1831 << "fallback to the built-in DCMTK decoder"; 1832 } 1833 } 1834 #endif 1835 1836 if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) 1837 { 1838 ServerContext::DicomCacheLocker locker(*this, publicId); 1839 return locker.GetDicom().DecodeFrame(frameIndex); 1840 } 1841 else 1842 { 1843 return NULL; // Built-in decoder is disabled 1844 } 1845 } 1846 1847 DecodeDicomFrame(const DicomInstanceToStore & dicom,unsigned int frameIndex)1848 ImageAccessor* ServerContext::DecodeDicomFrame(const DicomInstanceToStore& dicom, 1849 unsigned int frameIndex) 1850 { 1851 if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) 1852 { 1853 std::unique_ptr<ImageAccessor> decoded; 1854 try 1855 { 1856 decoded.reset(dicom.DecodeFrame(frameIndex)); 1857 } 1858 catch (OrthancException& e) 1859 { 1860 } 1861 1862 if (decoded.get() != NULL) 1863 { 1864 return decoded.release(); 1865 } 1866 } 1867 1868 #if ORTHANC_ENABLE_PLUGINS == 1 1869 if (HasPlugins() && 1870 GetPlugins().HasCustomImageDecoder()) 1871 { 1872 std::unique_ptr<ImageAccessor> decoded; 1873 try 1874 { 1875 decoded.reset(GetPlugins().Decode(dicom.GetBufferData(), dicom.GetBufferSize(), frameIndex)); 1876 } 1877 catch (OrthancException& e) 1878 { 1879 } 1880 1881 if (decoded.get() != NULL) 1882 { 1883 return decoded.release(); 1884 } 1885 else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) 1886 { 1887 LOG(INFO) << "The installed image decoding plugins cannot handle an image, " 1888 << "fallback to the built-in DCMTK decoder"; 1889 } 1890 } 1891 #endif 1892 1893 if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) 1894 { 1895 return dicom.DecodeFrame(frameIndex); 1896 } 1897 else 1898 { 1899 return NULL; 1900 } 1901 } 1902 1903 DecodeDicomFrame(const void * dicom,size_t size,unsigned int frameIndex)1904 ImageAccessor* ServerContext::DecodeDicomFrame(const void* dicom, 1905 size_t size, 1906 unsigned int frameIndex) 1907 { 1908 std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(dicom, size)); 1909 return DecodeDicomFrame(*instance, frameIndex); 1910 } 1911 1912 StoreWithTranscoding(std::string & sopClassUid,std::string & sopInstanceUid,DicomStoreUserConnection & connection,const std::string & dicom,bool hasMoveOriginator,const std::string & moveOriginatorAet,uint16_t moveOriginatorId)1913 void ServerContext::StoreWithTranscoding(std::string& sopClassUid, 1914 std::string& sopInstanceUid, 1915 DicomStoreUserConnection& connection, 1916 const std::string& dicom, 1917 bool hasMoveOriginator, 1918 const std::string& moveOriginatorAet, 1919 uint16_t moveOriginatorId) 1920 { 1921 const void* data = dicom.empty() ? NULL : dicom.c_str(); 1922 1923 if (!transcodeDicomProtocol_ || 1924 !connection.GetParameters().GetRemoteModality().IsTranscodingAllowed()) 1925 { 1926 connection.Store(sopClassUid, sopInstanceUid, data, dicom.size(), 1927 hasMoveOriginator, moveOriginatorAet, moveOriginatorId); 1928 } 1929 else 1930 { 1931 connection.Transcode(sopClassUid, sopInstanceUid, *this, data, dicom.size(), preferredTransferSyntax_, 1932 hasMoveOriginator, moveOriginatorAet, moveOriginatorId); 1933 } 1934 } 1935 1936 Transcode(DicomImage & target,DicomImage & source,const std::set<DicomTransferSyntax> & allowedSyntaxes,bool allowNewSopInstanceUid)1937 bool ServerContext::Transcode(DicomImage& target, 1938 DicomImage& source /* in, "GetParsed()" possibly modified */, 1939 const std::set<DicomTransferSyntax>& allowedSyntaxes, 1940 bool allowNewSopInstanceUid) 1941 { 1942 if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) 1943 { 1944 if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid)) 1945 { 1946 return true; 1947 } 1948 } 1949 1950 #if ORTHANC_ENABLE_PLUGINS == 1 1951 if (HasPlugins() && 1952 GetPlugins().HasCustomTranscoder()) 1953 { 1954 if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid)) 1955 { 1956 return true; 1957 } 1958 else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) 1959 { 1960 LOG(INFO) << "The installed transcoding plugins cannot handle an image, " 1961 << "fallback to the built-in DCMTK transcoder"; 1962 } 1963 } 1964 #endif 1965 1966 if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) 1967 { 1968 return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid); 1969 } 1970 else 1971 { 1972 return false; 1973 } 1974 } 1975 GetDeidentifiedContent(const DicomElement & element) const1976 const std::string& ServerContext::GetDeidentifiedContent(const DicomElement &element) const 1977 { 1978 static const std::string redactedContent = "*** POTENTIAL PHI ***"; 1979 1980 const DicomTag& tag = element.GetTag(); 1981 if (deidentifyLogs_ && ( 1982 logsDeidentifierRules_.IsCleared(tag) || 1983 logsDeidentifierRules_.IsRemoved(tag) || 1984 logsDeidentifierRules_.IsReplaced(tag))) 1985 { 1986 return redactedContent; 1987 } 1988 else 1989 { 1990 return element.GetValue().GetContent(); 1991 } 1992 } 1993 1994 GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax> & syntaxes)1995 void ServerContext::GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) 1996 { 1997 boost::mutex::scoped_lock lock(dynamicOptionsMutex_); 1998 syntaxes = acceptedTransferSyntaxes_; 1999 } 2000 2001 SetAcceptedTransferSyntaxes(const std::set<DicomTransferSyntax> & syntaxes)2002 void ServerContext::SetAcceptedTransferSyntaxes(const std::set<DicomTransferSyntax>& syntaxes) 2003 { 2004 boost::mutex::scoped_lock lock(dynamicOptionsMutex_); 2005 acceptedTransferSyntaxes_ = syntaxes; 2006 } 2007 2008 IsUnknownSopClassAccepted()2009 bool ServerContext::IsUnknownSopClassAccepted() 2010 { 2011 boost::mutex::scoped_lock lock(dynamicOptionsMutex_); 2012 return isUnknownSopClassAccepted_; 2013 } 2014 2015 SetUnknownSopClassAccepted(bool accepted)2016 void ServerContext::SetUnknownSopClassAccepted(bool accepted) 2017 { 2018 boost::mutex::scoped_lock lock(dynamicOptionsMutex_); 2019 isUnknownSopClassAccepted_ = accepted; 2020 } 2021 } 2022