Mercurial > hg > orthanc
comparison OrthancServer/ServerIndex.cpp @ 3160:fc9a4a2dad63
merge
author | Alain Mazy <alain@mazy.be> |
---|---|
date | Thu, 24 Jan 2019 10:55:19 +0100 |
parents | 2e1711f80f74 |
children | 8ea7c4546c3a |
comparison
equal
deleted
inserted
replaced
3159:4cfed5c2eacd | 3160:fc9a4a2dad63 |
---|---|
36 | 36 |
37 #ifndef NOMINMAX | 37 #ifndef NOMINMAX |
38 #define NOMINMAX | 38 #define NOMINMAX |
39 #endif | 39 #endif |
40 | 40 |
41 #include "ServerIndexChange.h" | 41 #include "../Core/DicomFormat/DicomArray.h" |
42 #include "../Core/DicomParsing/FromDcmtkBridge.h" | |
43 #include "../Core/DicomParsing/ParsedDicomFile.h" | |
44 #include "../Core/Logging.h" | |
45 #include "../Core/Toolbox.h" | |
46 | |
47 #include "Database/ResourcesContent.h" | |
48 #include "DicomInstanceToStore.h" | |
42 #include "EmbeddedResources.h" | 49 #include "EmbeddedResources.h" |
43 #include "OrthancConfiguration.h" | 50 #include "OrthancConfiguration.h" |
44 #include "../Core/DicomParsing/ParsedDicomFile.h" | 51 #include "Search/DatabaseLookup.h" |
52 #include "Search/DicomTagConstraint.h" | |
53 #include "ServerContext.h" | |
54 #include "ServerIndexChange.h" | |
45 #include "ServerToolbox.h" | 55 #include "ServerToolbox.h" |
46 #include "../Core/Toolbox.h" | |
47 #include "../Core/Logging.h" | |
48 #include "../Core/DicomFormat/DicomArray.h" | |
49 | |
50 #include "../Core/DicomParsing/FromDcmtkBridge.h" | |
51 #include "ServerContext.h" | |
52 #include "DicomInstanceToStore.h" | |
53 #include "Search/LookupResource.h" | |
54 | 56 |
55 #include <boost/lexical_cast.hpp> | 57 #include <boost/lexical_cast.hpp> |
56 #include <stdio.h> | 58 #include <stdio.h> |
57 | 59 |
58 static const uint64_t MEGA_BYTES = 1024 * 1024; | 60 static const uint64_t MEGA_BYTES = 1024 * 1024; |
59 | 61 |
60 namespace Orthanc | 62 namespace Orthanc |
61 { | 63 { |
64 static void CopyListToVector(std::vector<std::string>& target, | |
65 const std::list<std::string>& source) | |
66 { | |
67 target.resize(source.size()); | |
68 | |
69 size_t pos = 0; | |
70 | |
71 for (std::list<std::string>::const_iterator | |
72 it = source.begin(); it != source.end(); ++it) | |
73 { | |
74 target[pos] = *it; | |
75 pos ++; | |
76 } | |
77 } | |
78 | |
79 | |
62 class ServerIndex::Listener : public IDatabaseListener | 80 class ServerIndex::Listener : public IDatabaseListener |
63 { | 81 { |
64 private: | 82 private: |
65 struct FileToRemove | 83 struct FileToRemove |
66 { | 84 { |
213 | 231 |
214 class ServerIndex::Transaction | 232 class ServerIndex::Transaction |
215 { | 233 { |
216 private: | 234 private: |
217 ServerIndex& index_; | 235 ServerIndex& index_; |
218 std::auto_ptr<SQLite::ITransaction> transaction_; | 236 std::auto_ptr<IDatabaseWrapper::ITransaction> transaction_; |
219 bool isCommitted_; | 237 bool isCommitted_; |
220 | 238 |
221 public: | 239 public: |
222 Transaction(ServerIndex& index) : | 240 Transaction(ServerIndex& index) : |
223 index_(index), | 241 index_(index), |
224 isCommitted_(false) | 242 isCommitted_(false) |
225 { | 243 { |
226 transaction_.reset(index_.db_.StartTransaction()); | 244 transaction_.reset(index_.db_.StartTransaction()); |
227 transaction_->Begin(); | 245 transaction_->Begin(); |
228 | 246 |
229 assert(index_.currentStorageSize_ == index_.db_.GetTotalCompressedSize()); | |
230 | |
231 index_.listener_->StartTransaction(); | 247 index_.listener_->StartTransaction(); |
232 } | 248 } |
233 | 249 |
234 ~Transaction() | 250 ~Transaction() |
235 { | 251 { |
243 | 259 |
244 void Commit(uint64_t sizeOfAddedFiles) | 260 void Commit(uint64_t sizeOfAddedFiles) |
245 { | 261 { |
246 if (!isCommitted_) | 262 if (!isCommitted_) |
247 { | 263 { |
248 transaction_->Commit(); | 264 int64_t delta = (static_cast<int64_t>(sizeOfAddedFiles) - |
265 static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove())); | |
266 | |
267 transaction_->Commit(delta); | |
249 | 268 |
250 // We can remove the files once the SQLite transaction has | 269 // We can remove the files once the SQLite transaction has |
251 // been successfully committed. Some files might have to be | 270 // been successfully committed. Some files might have to be |
252 // deleted because of recycling. | 271 // deleted because of recycling. |
253 index_.listener_->CommitFilesToRemove(); | 272 index_.listener_->CommitFilesToRemove(); |
254 | 273 |
255 index_.currentStorageSize_ += sizeOfAddedFiles; | |
256 | |
257 assert(index_.currentStorageSize_ >= index_.listener_->GetSizeOfFilesToRemove()); | |
258 index_.currentStorageSize_ -= index_.listener_->GetSizeOfFilesToRemove(); | |
259 | |
260 // Send all the pending changes to the Orthanc plugins | 274 // Send all the pending changes to the Orthanc plugins |
261 index_.listener_->CommitChanges(); | 275 index_.listener_->CommitChanges(); |
262 | 276 |
263 isCommitted_ = true; | 277 isCommitted_ = true; |
264 } | 278 } |
301 return publicId_; | 315 return publicId_; |
302 } | 316 } |
303 }; | 317 }; |
304 | 318 |
305 | 319 |
320 class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable | |
321 { | |
322 private: | |
323 class TagInfo | |
324 { | |
325 private: | |
326 ResourceType level_; | |
327 DicomTagType type_; | |
328 | |
329 public: | |
330 TagInfo() | |
331 { | |
332 } | |
333 | |
334 TagInfo(ResourceType level, | |
335 DicomTagType type) : | |
336 level_(level), | |
337 type_(type) | |
338 { | |
339 } | |
340 | |
341 ResourceType GetLevel() const | |
342 { | |
343 return level_; | |
344 } | |
345 | |
346 DicomTagType GetType() const | |
347 { | |
348 return type_; | |
349 } | |
350 }; | |
351 | |
352 typedef std::map<DicomTag, TagInfo> Registry; | |
353 | |
354 | |
355 Registry registry_; | |
356 | |
357 void LoadTags(ResourceType level) | |
358 { | |
359 const DicomTag* tags = NULL; | |
360 size_t size; | |
361 | |
362 ServerToolbox::LoadIdentifiers(tags, size, level); | |
363 | |
364 for (size_t i = 0; i < size; i++) | |
365 { | |
366 if (registry_.find(tags[i]) == registry_.end()) | |
367 { | |
368 registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); | |
369 } | |
370 else | |
371 { | |
372 // These patient-level tags are copied in the study level | |
373 assert(level == ResourceType_Study && | |
374 (tags[i] == DICOM_TAG_PATIENT_ID || | |
375 tags[i] == DICOM_TAG_PATIENT_NAME || | |
376 tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); | |
377 } | |
378 } | |
379 | |
380 DicomMap::LoadMainDicomTags(tags, size, level); | |
381 | |
382 for (size_t i = 0; i < size; i++) | |
383 { | |
384 if (registry_.find(tags[i]) == registry_.end()) | |
385 { | |
386 registry_[tags[i]] = TagInfo(level, DicomTagType_Main); | |
387 } | |
388 } | |
389 } | |
390 | |
391 public: | |
392 MainDicomTagsRegistry() | |
393 { | |
394 LoadTags(ResourceType_Patient); | |
395 LoadTags(ResourceType_Study); | |
396 LoadTags(ResourceType_Series); | |
397 LoadTags(ResourceType_Instance); | |
398 } | |
399 | |
400 void LookupTag(ResourceType& level, | |
401 DicomTagType& type, | |
402 const DicomTag& tag) const | |
403 { | |
404 Registry::const_iterator it = registry_.find(tag); | |
405 | |
406 if (it == registry_.end()) | |
407 { | |
408 // Default values | |
409 level = ResourceType_Instance; | |
410 type = DicomTagType_Generic; | |
411 } | |
412 else | |
413 { | |
414 level = it->second.GetLevel(); | |
415 type = it->second.GetType(); | |
416 } | |
417 } | |
418 }; | |
419 | |
420 | |
306 bool ServerIndex::DeleteResource(Json::Value& target, | 421 bool ServerIndex::DeleteResource(Json::Value& target, |
307 const std::string& uuid, | 422 const std::string& uuid, |
308 ResourceType expectedType) | 423 ResourceType expectedType) |
309 { | 424 { |
310 boost::mutex::scoped_lock lock(mutex_); | 425 boost::mutex::scoped_lock lock(mutex_); |
385 | 500 |
386 LOG(INFO) << "Stopping the database flushing thread"; | 501 LOG(INFO) << "Stopping the database flushing thread"; |
387 } | 502 } |
388 | 503 |
389 | 504 |
390 static void ComputeExpectedNumberOfInstances(IDatabaseWrapper& db, | 505 static bool ComputeExpectedNumberOfInstances(int64_t& target, |
391 int64_t series, | |
392 const DicomMap& dicomSummary) | 506 const DicomMap& dicomSummary) |
393 { | 507 { |
394 try | 508 try |
395 { | 509 { |
396 const DicomValue* value; | 510 const DicomValue* value; |
397 const DicomValue* value2; | 511 const DicomValue* value2; |
398 | 512 |
399 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL && | 513 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL && |
400 (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL) | 514 !value->IsNull() && |
515 !value->IsBinary() && | |
516 (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL && | |
517 !value2->IsNull() && | |
518 !value2->IsBinary()) | |
401 { | 519 { |
402 // Patch for series with temporal positions thanks to Will Ryder | 520 // Patch for series with temporal positions thanks to Will Ryder |
403 int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent()); | 521 int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent()); |
404 int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent()); | 522 int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent()); |
405 std::string expected = boost::lexical_cast<std::string>(imagesInAcquisition * countTemporalPositions); | 523 target = imagesInAcquisition * countTemporalPositions; |
406 db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, expected); | 524 return (target > 0); |
407 } | 525 } |
408 | 526 |
409 else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL && | 527 else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL && |
410 (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL) | 528 !value->IsNull() && |
529 !value->IsBinary() && | |
530 (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL && | |
531 !value2->IsBinary() && | |
532 !value2->IsNull()) | |
411 { | 533 { |
412 // Support of Cardio-PET images | 534 // Support of Cardio-PET images |
413 int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent()); | 535 int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent()); |
414 int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent()); | 536 int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent()); |
415 std::string expected = boost::lexical_cast<std::string>(numberOfSlices * numberOfTimeSlices); | 537 target = numberOfSlices * numberOfTimeSlices; |
416 db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, expected); | 538 return (target > 0); |
417 } | 539 } |
418 | 540 |
419 else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL) | 541 else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL && |
420 { | 542 !value->IsNull() && |
421 db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, value->GetContent()); | 543 !value->IsBinary()) |
544 { | |
545 target = boost::lexical_cast<int64_t>(value->GetContent()); | |
546 return (target > 0); | |
422 } | 547 } |
423 } | 548 } |
424 catch (OrthancException&) | 549 catch (OrthancException&) |
425 { | 550 { |
426 } | 551 } |
427 catch (boost::bad_lexical_cast&) | 552 catch (boost::bad_lexical_cast&) |
428 { | 553 { |
429 } | 554 } |
555 | |
556 return false; | |
430 } | 557 } |
431 | 558 |
432 | 559 |
433 | 560 |
434 | 561 |
496 db_.SetGlobalProperty(property, "1"); | 623 db_.SetGlobalProperty(property, "1"); |
497 return 1; | 624 return 1; |
498 } | 625 } |
499 } | 626 } |
500 | 627 |
501 | |
502 | |
503 int64_t ServerIndex::CreateResource(const std::string& publicId, | |
504 ResourceType type) | |
505 { | |
506 int64_t id = db_.CreateResource(publicId, type); | |
507 | |
508 ChangeType changeType; | |
509 switch (type) | |
510 { | |
511 case ResourceType_Patient: | |
512 changeType = ChangeType_NewPatient; | |
513 break; | |
514 | |
515 case ResourceType_Study: | |
516 changeType = ChangeType_NewStudy; | |
517 break; | |
518 | |
519 case ResourceType_Series: | |
520 changeType = ChangeType_NewSeries; | |
521 break; | |
522 | |
523 case ResourceType_Instance: | |
524 changeType = ChangeType_NewInstance; | |
525 break; | |
526 | |
527 default: | |
528 throw OrthancException(ErrorCode_InternalError); | |
529 } | |
530 | |
531 ServerIndexChange change(changeType, type, publicId); | |
532 db_.LogChange(id, change); | |
533 | |
534 assert(listener_.get() != NULL); | |
535 listener_->SignalChange(change); | |
536 | |
537 return id; | |
538 } | |
539 | 628 |
540 | 629 |
541 ServerIndex::ServerIndex(ServerContext& context, | 630 ServerIndex::ServerIndex(ServerContext& context, |
542 IDatabaseWrapper& db, | 631 IDatabaseWrapper& db, |
543 unsigned int threadSleep) : | 632 unsigned int threadSleep) : |
544 done_(false), | 633 done_(false), |
545 db_(db), | 634 db_(db), |
546 maximumStorageSize_(0), | 635 maximumStorageSize_(0), |
547 maximumPatients_(0), | 636 maximumPatients_(0), |
548 overwrite_(false) | 637 overwrite_(false), |
638 mainDicomTagsRegistry_(new MainDicomTagsRegistry) | |
549 { | 639 { |
550 listener_.reset(new Listener(context)); | 640 listener_.reset(new Listener(context)); |
551 db_.SetListener(*listener_); | 641 db_.SetListener(*listener_); |
552 | |
553 currentStorageSize_ = db_.GetTotalCompressedSize(); | |
554 | 642 |
555 // Initial recycling if the parameters have changed since the last | 643 // Initial recycling if the parameters have changed since the last |
556 // execution of Orthanc | 644 // execution of Orthanc |
557 StandaloneRecycling(); | 645 StandaloneRecycling(); |
558 | 646 |
596 } | 684 } |
597 } | 685 } |
598 } | 686 } |
599 | 687 |
600 | 688 |
601 | 689 static void SetInstanceMetadata(ResourcesContent& content, |
602 void ServerIndex::SetInstanceMetadata(std::map<MetadataType, std::string>& instanceMetadata, | 690 std::map<MetadataType, std::string>& instanceMetadata, |
603 int64_t instance, | 691 int64_t instance, |
604 MetadataType metadata, | 692 MetadataType metadata, |
605 const std::string& value) | 693 const std::string& value) |
606 { | 694 { |
607 db_.SetMetadata(instance, metadata, value); | 695 content.AddMetadata(instance, metadata, value); |
608 instanceMetadata[metadata] = value; | 696 instanceMetadata[metadata] = value; |
609 } | 697 } |
610 | 698 |
611 | 699 |
612 | 700 void ServerIndex::SignalNewResource(ChangeType changeType, |
701 ResourceType level, | |
702 const std::string& publicId, | |
703 int64_t internalId) | |
704 { | |
705 ServerIndexChange change(changeType, level, publicId); | |
706 db_.LogChange(internalId, change); | |
707 | |
708 assert(listener_.get() != NULL); | |
709 listener_->SignalChange(change); | |
710 } | |
711 | |
712 | |
613 StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata, | 713 StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata, |
614 DicomInstanceToStore& instanceToStore, | 714 DicomInstanceToStore& instanceToStore, |
615 const Attachments& attachments) | 715 const Attachments& attachments) |
616 { | 716 { |
617 boost::mutex::scoped_lock lock(mutex_); | 717 boost::mutex::scoped_lock lock(mutex_); |
618 | 718 |
619 const DicomMap& dicomSummary = instanceToStore.GetSummary(); | 719 const DicomMap& dicomSummary = instanceToStore.GetSummary(); |
620 const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata(); | 720 const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata(); |
621 | 721 |
722 int64_t expectedInstances; | |
723 const bool hasExpectedInstances = | |
724 ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary); | |
725 | |
622 instanceMetadata.clear(); | 726 instanceMetadata.clear(); |
623 | 727 |
728 const std::string hashPatient = instanceToStore.GetHasher().HashPatient(); | |
729 const std::string hashStudy = instanceToStore.GetHasher().HashStudy(); | |
730 const std::string hashSeries = instanceToStore.GetHasher().HashSeries(); | |
731 const std::string hashInstance = instanceToStore.GetHasher().HashInstance(); | |
732 | |
624 try | 733 try |
625 { | 734 { |
626 Transaction t(*this); | 735 Transaction t(*this); |
627 | 736 |
737 IDatabaseWrapper::CreateInstanceResult status; | |
738 int64_t instanceId; | |
739 | |
628 // Check whether this instance is already stored | 740 // Check whether this instance is already stored |
629 { | 741 if (!db_.CreateInstance(status, instanceId, hashPatient, |
630 ResourceType type; | 742 hashStudy, hashSeries, hashInstance)) |
631 int64_t tmp; | 743 { |
632 if (db_.LookupResource(tmp, type, instanceToStore.GetHasher().HashInstance())) | 744 // The instance already exists |
633 { | 745 |
634 assert(type == ResourceType_Instance); | 746 if (overwrite_) |
635 | 747 { |
636 if (overwrite_) | 748 // Overwrite the old instance |
749 LOG(INFO) << "Overwriting instance: " << hashInstance; | |
750 db_.DeleteResource(instanceId); | |
751 | |
752 // Re-create the instance, now that the old one is removed | |
753 if (!db_.CreateInstance(status, instanceId, hashPatient, | |
754 hashStudy, hashSeries, hashInstance)) | |
637 { | 755 { |
638 // Overwrite the old instance | 756 throw OrthancException(ErrorCode_InternalError); |
639 LOG(INFO) << "Overwriting instance: " << instanceToStore.GetHasher().HashInstance(); | |
640 db_.DeleteResource(tmp); | |
641 } | 757 } |
642 else | 758 } |
643 { | 759 else |
644 // Do nothing if the instance already exists | 760 { |
645 db_.GetAllMetadata(instanceMetadata, tmp); | 761 // Do nothing if the instance already exists and overwriting is disabled |
646 return StoreStatus_AlreadyStored; | 762 db_.GetAllMetadata(instanceMetadata, instanceId); |
647 } | 763 return StoreStatus_AlreadyStored; |
648 } | 764 } |
649 } | 765 } |
650 | 766 |
767 | |
768 // Warn about the creation of new resources. The order must be | |
769 // from instance to patient. | |
770 | |
771 // NB: In theory, could be sped up by grouping the underlying | |
772 // calls to "db_.LogChange()". However, this would only have an | |
773 // impact when new patient/study/series get created, which | |
774 // occurs far less often that creating new instances. The | |
775 // positive impact looks marginal in practice. | |
776 SignalNewResource(ChangeType_NewInstance, ResourceType_Instance, hashInstance, instanceId); | |
777 | |
778 if (status.isNewSeries_) | |
779 { | |
780 SignalNewResource(ChangeType_NewSeries, ResourceType_Series, hashSeries, status.seriesId_); | |
781 } | |
782 | |
783 if (status.isNewStudy_) | |
784 { | |
785 SignalNewResource(ChangeType_NewStudy, ResourceType_Study, hashStudy, status.studyId_); | |
786 } | |
787 | |
788 if (status.isNewPatient_) | |
789 { | |
790 SignalNewResource(ChangeType_NewPatient, ResourceType_Patient, hashPatient, status.patientId_); | |
791 } | |
792 | |
793 | |
651 // Ensure there is enough room in the storage for the new instance | 794 // Ensure there is enough room in the storage for the new instance |
652 uint64_t instanceSize = 0; | 795 uint64_t instanceSize = 0; |
653 for (Attachments::const_iterator it = attachments.begin(); | 796 for (Attachments::const_iterator it = attachments.begin(); |
654 it != attachments.end(); ++it) | 797 it != attachments.end(); ++it) |
655 { | 798 { |
656 instanceSize += it->GetCompressedSize(); | 799 instanceSize += it->GetCompressedSize(); |
657 } | 800 } |
658 | 801 |
659 Recycle(instanceSize, instanceToStore.GetHasher().HashPatient()); | 802 Recycle(instanceSize, hashPatient /* don't consider the current patient for recycling */); |
660 | 803 |
661 // Create the instance | 804 |
662 int64_t instance = CreateResource(instanceToStore.GetHasher().HashInstance(), ResourceType_Instance); | |
663 ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, dicomSummary); | |
664 | |
665 // Detect up to which level the patient/study/series/instance | |
666 // hierarchy must be created | |
667 int64_t patient = -1, study = -1, series = -1; | |
668 bool isNewPatient = false; | |
669 bool isNewStudy = false; | |
670 bool isNewSeries = false; | |
671 | |
672 { | |
673 ResourceType dummy; | |
674 | |
675 if (db_.LookupResource(series, dummy, instanceToStore.GetHasher().HashSeries())) | |
676 { | |
677 assert(dummy == ResourceType_Series); | |
678 // The patient, the study and the series already exist | |
679 | |
680 bool ok = (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()) && | |
681 db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy())); | |
682 assert(ok); | |
683 } | |
684 else if (db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy())) | |
685 { | |
686 assert(dummy == ResourceType_Study); | |
687 | |
688 // New series: The patient and the study already exist | |
689 isNewSeries = true; | |
690 | |
691 bool ok = db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()); | |
692 assert(ok); | |
693 } | |
694 else if (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient())) | |
695 { | |
696 assert(dummy == ResourceType_Patient); | |
697 | |
698 // New study and series: The patient already exist | |
699 isNewStudy = true; | |
700 isNewSeries = true; | |
701 } | |
702 else | |
703 { | |
704 // New patient, study and series: Nothing exists | |
705 isNewPatient = true; | |
706 isNewStudy = true; | |
707 isNewSeries = true; | |
708 } | |
709 } | |
710 | |
711 // Create the series if needed | |
712 if (isNewSeries) | |
713 { | |
714 series = CreateResource(instanceToStore.GetHasher().HashSeries(), ResourceType_Series); | |
715 ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, dicomSummary); | |
716 } | |
717 | |
718 // Create the study if needed | |
719 if (isNewStudy) | |
720 { | |
721 study = CreateResource(instanceToStore.GetHasher().HashStudy(), ResourceType_Study); | |
722 ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, dicomSummary); | |
723 } | |
724 | |
725 // Create the patient if needed | |
726 if (isNewPatient) | |
727 { | |
728 patient = CreateResource(instanceToStore.GetHasher().HashPatient(), ResourceType_Patient); | |
729 ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, dicomSummary); | |
730 } | |
731 | |
732 // Create the parent-to-child links | |
733 db_.AttachChild(series, instance); | |
734 | |
735 if (isNewSeries) | |
736 { | |
737 db_.AttachChild(study, series); | |
738 } | |
739 | |
740 if (isNewStudy) | |
741 { | |
742 db_.AttachChild(patient, study); | |
743 } | |
744 | |
745 // Sanity checks | |
746 assert(patient != -1); | |
747 assert(study != -1); | |
748 assert(series != -1); | |
749 assert(instance != -1); | |
750 | |
751 // Attach the files to the newly created instance | 805 // Attach the files to the newly created instance |
752 for (Attachments::const_iterator it = attachments.begin(); | 806 for (Attachments::const_iterator it = attachments.begin(); |
753 it != attachments.end(); ++it) | 807 it != attachments.end(); ++it) |
754 { | 808 { |
755 db_.AddAttachment(instance, *it); | 809 db_.AddAttachment(instanceId, *it); |
756 } | 810 } |
757 | 811 |
758 // Attach the user-specified metadata | 812 |
759 for (MetadataMap::const_iterator | 813 { |
760 it = metadata.begin(); it != metadata.end(); ++it) | 814 ResourcesContent content; |
761 { | 815 |
762 switch (it->first.first) | 816 // Populate the tags of the newly-created resources |
763 { | 817 |
764 case ResourceType_Patient: | 818 content.AddResource(instanceId, ResourceType_Instance, dicomSummary); |
765 db_.SetMetadata(patient, it->first.second, it->second); | 819 |
766 break; | 820 if (status.isNewSeries_) |
767 | 821 { |
768 case ResourceType_Study: | 822 content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary); |
769 db_.SetMetadata(study, it->first.second, it->second); | 823 } |
770 break; | 824 |
771 | 825 if (status.isNewStudy_) |
772 case ResourceType_Series: | 826 { |
773 db_.SetMetadata(series, it->first.second, it->second); | 827 content.AddResource(status.studyId_, ResourceType_Study, dicomSummary); |
774 break; | 828 } |
775 | 829 |
776 case ResourceType_Instance: | 830 if (status.isNewPatient_) |
777 SetInstanceMetadata(instanceMetadata, instance, it->first.second, it->second); | 831 { |
778 break; | 832 content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary); |
779 | 833 } |
780 default: | 834 |
781 throw OrthancException(ErrorCode_ParameterOutOfRange); | 835 |
782 } | 836 // Attach the user-specified metadata |
783 } | 837 |
784 | 838 for (MetadataMap::const_iterator |
785 // Attach the auto-computed metadata for the patient/study/series levels | 839 it = metadata.begin(); it != metadata.end(); ++it) |
786 std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */); | 840 { |
787 db_.SetMetadata(series, MetadataType_LastUpdate, now); | 841 switch (it->first.first) |
788 db_.SetMetadata(study, MetadataType_LastUpdate, now); | 842 { |
789 db_.SetMetadata(patient, MetadataType_LastUpdate, now); | 843 case ResourceType_Patient: |
790 | 844 content.AddMetadata(status.patientId_, it->first.second, it->second); |
791 // Attach the auto-computed metadata for the instance level, | 845 break; |
792 // reflecting these additions into the input metadata map | 846 |
793 SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_ReceptionDate, now); | 847 case ResourceType_Study: |
794 SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet, | 848 content.AddMetadata(status.studyId_, it->first.second, it->second); |
795 instanceToStore.GetOrigin().GetRemoteAetC()); | 849 break; |
796 SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_Origin, | 850 |
797 EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin())); | 851 case ResourceType_Series: |
798 | 852 content.AddMetadata(status.seriesId_, it->first.second, it->second); |
799 { | 853 break; |
800 std::string s; | 854 |
801 | 855 case ResourceType_Instance: |
802 if (instanceToStore.LookupTransferSyntax(s)) | 856 SetInstanceMetadata(content, instanceMetadata, instanceId, |
803 { | 857 it->first.second, it->second); |
804 // New in Orthanc 1.2.0 | 858 break; |
805 SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_TransferSyntax, s); | 859 |
806 } | 860 default: |
807 | 861 throw OrthancException(ErrorCode_ParameterOutOfRange); |
808 if (instanceToStore.GetOrigin().LookupRemoteIp(s)) | 862 } |
809 { | 863 } |
810 // New in Orthanc 1.4.0 | 864 |
811 SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteIp, s); | 865 |
812 } | 866 // Attach the auto-computed metadata for the patient/study/series levels |
813 | 867 std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */); |
814 if (instanceToStore.GetOrigin().LookupCalledAet(s)) | 868 content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now); |
815 { | 869 content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now); |
816 // New in Orthanc 1.4.0 | 870 content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now); |
817 SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_CalledAet, s); | 871 |
818 } | 872 if (status.isNewSeries_ && |
819 | 873 hasExpectedInstances) |
820 if (instanceToStore.GetOrigin().LookupHttpUsername(s)) | 874 { |
821 { | 875 content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances, |
822 // New in Orthanc 1.4.0 | 876 boost::lexical_cast<std::string>(expectedInstances)); |
823 SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_HttpUsername, s); | 877 } |
824 } | 878 |
825 } | 879 |
826 | 880 // Attach the auto-computed metadata for the instance level, |
827 const DicomValue* value; | 881 // reflecting these additions into the input metadata map |
828 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && | 882 SetInstanceMetadata(content, instanceMetadata, instanceId, |
829 !value->IsNull() && | 883 MetadataType_Instance_ReceptionDate, now); |
830 !value->IsBinary()) | 884 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_RemoteAet, |
831 { | 885 instanceToStore.GetOrigin().GetRemoteAetC()); |
832 SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_SopClassUid, value->GetContent()); | 886 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin, |
833 } | 887 EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin())); |
834 | 888 |
835 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || | 889 |
836 (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) | 890 { |
837 { | 891 std::string s; |
838 if (!value->IsNull() && | 892 |
893 if (instanceToStore.LookupTransferSyntax(s)) | |
894 { | |
895 // New in Orthanc 1.2.0 | |
896 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
897 MetadataType_Instance_TransferSyntax, s); | |
898 } | |
899 | |
900 if (instanceToStore.GetOrigin().LookupRemoteIp(s)) | |
901 { | |
902 // New in Orthanc 1.4.0 | |
903 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
904 MetadataType_Instance_RemoteIp, s); | |
905 } | |
906 | |
907 if (instanceToStore.GetOrigin().LookupCalledAet(s)) | |
908 { | |
909 // New in Orthanc 1.4.0 | |
910 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
911 MetadataType_Instance_CalledAet, s); | |
912 } | |
913 | |
914 if (instanceToStore.GetOrigin().LookupHttpUsername(s)) | |
915 { | |
916 // New in Orthanc 1.4.0 | |
917 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
918 MetadataType_Instance_HttpUsername, s); | |
919 } | |
920 } | |
921 | |
922 | |
923 const DicomValue* value; | |
924 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && | |
925 !value->IsNull() && | |
839 !value->IsBinary()) | 926 !value->IsBinary()) |
840 { | 927 { |
841 SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_IndexInSeries, value->GetContent()); | 928 SetInstanceMetadata(content, instanceMetadata, instanceId, |
842 } | 929 MetadataType_Instance_SopClassUid, value->GetContent()); |
843 } | 930 } |
844 | 931 |
932 | |
933 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || | |
934 (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) | |
935 { | |
936 if (!value->IsNull() && | |
937 !value->IsBinary()) | |
938 { | |
939 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
940 MetadataType_Instance_IndexInSeries, value->GetContent()); | |
941 } | |
942 } | |
943 | |
944 | |
945 db_.SetResourcesContent(content); | |
946 } | |
947 | |
948 | |
845 // Check whether the series of this new instance is now completed | 949 // Check whether the series of this new instance is now completed |
846 if (isNewSeries) | 950 SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_); |
847 { | |
848 ComputeExpectedNumberOfInstances(db_, series, dicomSummary); | |
849 } | |
850 | |
851 SeriesStatus seriesStatus = GetSeriesStatus(series); | |
852 if (seriesStatus == SeriesStatus_Complete) | 951 if (seriesStatus == SeriesStatus_Complete) |
853 { | 952 { |
854 LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, instanceToStore.GetHasher().HashSeries()); | 953 LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries); |
855 } | 954 } |
955 | |
856 | 956 |
857 // Mark the parent resources of this instance as unstable | 957 // Mark the parent resources of this instance as unstable |
858 MarkAsUnstable(series, ResourceType_Series, instanceToStore.GetHasher().HashSeries()); | 958 MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries); |
859 MarkAsUnstable(study, ResourceType_Study, instanceToStore.GetHasher().HashStudy()); | 959 MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy); |
860 MarkAsUnstable(patient, ResourceType_Patient, instanceToStore.GetHasher().HashPatient()); | 960 MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient); |
861 | 961 |
862 t.Commit(instanceSize); | 962 t.Commit(instanceSize); |
863 | 963 |
864 return StoreStatus_Success; | 964 return StoreStatus_Success; |
865 } | 965 } |
875 void ServerIndex::ComputeStatistics(Json::Value& target) | 975 void ServerIndex::ComputeStatistics(Json::Value& target) |
876 { | 976 { |
877 boost::mutex::scoped_lock lock(mutex_); | 977 boost::mutex::scoped_lock lock(mutex_); |
878 target = Json::objectValue; | 978 target = Json::objectValue; |
879 | 979 |
880 uint64_t cs = currentStorageSize_; | 980 uint64_t cs = db_.GetTotalCompressedSize(); |
881 assert(cs == db_.GetTotalCompressedSize()); | |
882 uint64_t us = db_.GetTotalUncompressedSize(); | 981 uint64_t us = db_.GetTotalUncompressedSize(); |
883 target["TotalDiskSize"] = boost::lexical_cast<std::string>(cs); | 982 target["TotalDiskSize"] = boost::lexical_cast<std::string>(cs); |
884 target["TotalUncompressedSize"] = boost::lexical_cast<std::string>(us); | 983 target["TotalUncompressedSize"] = boost::lexical_cast<std::string>(us); |
885 target["TotalDiskSizeMB"] = static_cast<unsigned int>(cs / MEGA_BYTES); | 984 target["TotalDiskSizeMB"] = static_cast<unsigned int>(cs / MEGA_BYTES); |
886 target["TotalUncompressedSizeMB"] = static_cast<unsigned int>(us / MEGA_BYTES); | 985 target["TotalUncompressedSizeMB"] = static_cast<unsigned int>(us / MEGA_BYTES); |
890 target["CountSeries"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Series)); | 989 target["CountSeries"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Series)); |
891 target["CountInstances"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Instance)); | 990 target["CountInstances"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Instance)); |
892 } | 991 } |
893 | 992 |
894 | 993 |
994 | |
995 SeriesStatus ServerIndex::GetSeriesStatus(int64_t id, | |
996 int64_t expectedNumberOfInstances) | |
997 { | |
998 std::list<std::string> values; | |
999 db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries); | |
1000 | |
1001 std::set<int64_t> instances; | |
1002 | |
1003 for (std::list<std::string>::const_iterator | |
1004 it = values.begin(); it != values.end(); ++it) | |
1005 { | |
1006 int64_t index; | |
1007 | |
1008 try | |
1009 { | |
1010 index = boost::lexical_cast<int64_t>(*it); | |
1011 } | |
1012 catch (boost::bad_lexical_cast&) | |
1013 { | |
1014 return SeriesStatus_Unknown; | |
1015 } | |
1016 | |
1017 if (!(index > 0 && index <= expectedNumberOfInstances)) | |
1018 { | |
1019 // Out-of-range instance index | |
1020 return SeriesStatus_Inconsistent; | |
1021 } | |
1022 | |
1023 if (instances.find(index) != instances.end()) | |
1024 { | |
1025 // Twice the same instance index | |
1026 return SeriesStatus_Inconsistent; | |
1027 } | |
1028 | |
1029 instances.insert(index); | |
1030 } | |
1031 | |
1032 if (static_cast<int64_t>(instances.size()) == expectedNumberOfInstances) | |
1033 { | |
1034 return SeriesStatus_Complete; | |
1035 } | |
1036 else | |
1037 { | |
1038 return SeriesStatus_Missing; | |
1039 } | |
1040 } | |
1041 | |
895 | 1042 |
896 SeriesStatus ServerIndex::GetSeriesStatus(int64_t id) | 1043 SeriesStatus ServerIndex::GetSeriesStatus(int64_t id) |
897 { | 1044 { |
898 // Get the expected number of instances in this series (from the metadata) | 1045 // Get the expected number of instances in this series (from the metadata) |
899 int64_t expected; | 1046 int64_t expected; |
900 if (!GetMetadataAsInteger(expected, id, MetadataType_Series_ExpectedNumberOfInstances)) | 1047 if (!GetMetadataAsInteger(expected, id, MetadataType_Series_ExpectedNumberOfInstances)) |
901 { | 1048 { |
902 return SeriesStatus_Unknown; | 1049 return SeriesStatus_Unknown; |
903 } | 1050 } |
904 | |
905 // Loop over the instances of this series | |
906 std::list<int64_t> children; | |
907 db_.GetChildrenInternalId(children, id); | |
908 | |
909 std::set<int64_t> instances; | |
910 for (std::list<int64_t>::const_iterator | |
911 it = children.begin(); it != children.end(); ++it) | |
912 { | |
913 // Get the index of this instance in the series | |
914 int64_t index; | |
915 if (!GetMetadataAsInteger(index, *it, MetadataType_Instance_IndexInSeries)) | |
916 { | |
917 return SeriesStatus_Unknown; | |
918 } | |
919 | |
920 if (!(index > 0 && index <= expected)) | |
921 { | |
922 // Out-of-range instance index | |
923 return SeriesStatus_Inconsistent; | |
924 } | |
925 | |
926 if (instances.find(index) != instances.end()) | |
927 { | |
928 // Twice the same instance index | |
929 return SeriesStatus_Inconsistent; | |
930 } | |
931 | |
932 instances.insert(index); | |
933 } | |
934 | |
935 if (static_cast<int64_t>(instances.size()) == expected) | |
936 { | |
937 return SeriesStatus_Complete; | |
938 } | |
939 else | 1051 else |
940 { | 1052 { |
941 return SeriesStatus_Missing; | 1053 return GetSeriesStatus(id, expected); |
942 } | 1054 } |
943 } | 1055 } |
944 | 1056 |
945 | 1057 |
946 void ServerIndex::MainDicomTagsToJson(Json::Value& target, | 1058 void ServerIndex::MainDicomTagsToJson(Json::Value& target, |
967 target["MainDicomTags"] = Json::objectValue; | 1079 target["MainDicomTags"] = Json::objectValue; |
968 FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true); | 1080 FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true); |
969 } | 1081 } |
970 } | 1082 } |
971 | 1083 |
1084 | |
972 bool ServerIndex::LookupResource(Json::Value& result, | 1085 bool ServerIndex::LookupResource(Json::Value& result, |
973 const std::string& publicId, | 1086 const std::string& publicId, |
974 ResourceType expectedType) | 1087 ResourceType expectedType) |
975 { | 1088 { |
976 result = Json::objectValue; | 1089 result = Json::objectValue; |
1065 result["Type"] = "Series"; | 1178 result["Type"] = "Series"; |
1066 result["Status"] = EnumerationToString(GetSeriesStatus(id)); | 1179 result["Status"] = EnumerationToString(GetSeriesStatus(id)); |
1067 | 1180 |
1068 int64_t i; | 1181 int64_t i; |
1069 if (GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) | 1182 if (GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) |
1183 { | |
1070 result["ExpectedNumberOfInstances"] = static_cast<int>(i); | 1184 result["ExpectedNumberOfInstances"] = static_cast<int>(i); |
1185 } | |
1071 else | 1186 else |
1187 { | |
1072 result["ExpectedNumberOfInstances"] = Json::nullValue; | 1188 result["ExpectedNumberOfInstances"] = Json::nullValue; |
1189 } | |
1073 | 1190 |
1074 break; | 1191 break; |
1075 } | 1192 } |
1076 | 1193 |
1077 case ResourceType_Instance: | 1194 case ResourceType_Instance: |
1087 result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize()); | 1204 result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize()); |
1088 result["FileUuid"] = attachment.GetUuid(); | 1205 result["FileUuid"] = attachment.GetUuid(); |
1089 | 1206 |
1090 int64_t i; | 1207 int64_t i; |
1091 if (GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) | 1208 if (GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) |
1209 { | |
1092 result["IndexInSeries"] = static_cast<int>(i); | 1210 result["IndexInSeries"] = static_cast<int>(i); |
1211 } | |
1093 else | 1212 else |
1213 { | |
1094 result["IndexInSeries"] = Json::nullValue; | 1214 result["IndexInSeries"] = Json::nullValue; |
1215 } | |
1095 | 1216 |
1096 break; | 1217 break; |
1097 } | 1218 } |
1098 | 1219 |
1099 default: | 1220 default: |
1185 template <typename T> | 1306 template <typename T> |
1186 static void FormatLog(Json::Value& target, | 1307 static void FormatLog(Json::Value& target, |
1187 const std::list<T>& log, | 1308 const std::list<T>& log, |
1188 const std::string& name, | 1309 const std::string& name, |
1189 bool done, | 1310 bool done, |
1190 int64_t since) | 1311 int64_t since, |
1312 bool hasLast, | |
1313 int64_t last) | |
1191 { | 1314 { |
1192 Json::Value items = Json::arrayValue; | 1315 Json::Value items = Json::arrayValue; |
1193 for (typename std::list<T>::const_iterator | 1316 for (typename std::list<T>::const_iterator |
1194 it = log.begin(); it != log.end(); ++it) | 1317 it = log.begin(); it != log.end(); ++it) |
1195 { | 1318 { |
1200 | 1323 |
1201 target = Json::objectValue; | 1324 target = Json::objectValue; |
1202 target[name] = items; | 1325 target[name] = items; |
1203 target["Done"] = done; | 1326 target["Done"] = done; |
1204 | 1327 |
1205 int64_t last = (log.empty() ? since : log.back().GetSeq()); | 1328 if (!hasLast) |
1329 { | |
1330 // Best-effort guess of the last index in the sequence | |
1331 if (log.empty()) | |
1332 { | |
1333 last = since; | |
1334 } | |
1335 else | |
1336 { | |
1337 last = log.back().GetSeq(); | |
1338 } | |
1339 } | |
1340 | |
1206 target["Last"] = static_cast<int>(last); | 1341 target["Last"] = static_cast<int>(last); |
1207 } | 1342 } |
1208 | 1343 |
1209 | 1344 |
1210 void ServerIndex::GetChanges(Json::Value& target, | 1345 void ServerIndex::GetChanges(Json::Value& target, |
1211 int64_t since, | 1346 int64_t since, |
1212 unsigned int maxResults) | 1347 unsigned int maxResults) |
1213 { | 1348 { |
1214 std::list<ServerIndexChange> changes; | 1349 std::list<ServerIndexChange> changes; |
1215 bool done; | 1350 bool done; |
1351 bool hasLast = false; | |
1352 int64_t last = 0; | |
1216 | 1353 |
1217 { | 1354 { |
1218 boost::mutex::scoped_lock lock(mutex_); | 1355 boost::mutex::scoped_lock lock(mutex_); |
1219 | 1356 |
1220 // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as | 1357 // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as |
1221 // "GetLastChange()" involves calls to "GetPublicId()" | 1358 // "GetLastChange()" involves calls to "GetPublicId()" |
1222 Transaction transaction(*this); | 1359 Transaction transaction(*this); |
1360 | |
1223 db_.GetChanges(changes, done, since, maxResults); | 1361 db_.GetChanges(changes, done, since, maxResults); |
1362 if (changes.empty()) | |
1363 { | |
1364 last = db_.GetLastChangeIndex(); | |
1365 hasLast = true; | |
1366 } | |
1367 | |
1224 transaction.Commit(0); | 1368 transaction.Commit(0); |
1225 } | 1369 } |
1226 | 1370 |
1227 FormatLog(target, changes, "Changes", done, since); | 1371 FormatLog(target, changes, "Changes", done, since, hasLast, last); |
1228 } | 1372 } |
1229 | 1373 |
1230 | 1374 |
1231 void ServerIndex::GetLastChange(Json::Value& target) | 1375 void ServerIndex::GetLastChange(Json::Value& target) |
1232 { | 1376 { |
1233 std::list<ServerIndexChange> changes; | 1377 std::list<ServerIndexChange> changes; |
1378 bool hasLast = false; | |
1379 int64_t last = 0; | |
1234 | 1380 |
1235 { | 1381 { |
1236 boost::mutex::scoped_lock lock(mutex_); | 1382 boost::mutex::scoped_lock lock(mutex_); |
1237 | 1383 |
1238 // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as | 1384 // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as |
1239 // "GetLastChange()" involves calls to "GetPublicId()" | 1385 // "GetLastChange()" involves calls to "GetPublicId()" |
1240 Transaction transaction(*this); | 1386 Transaction transaction(*this); |
1387 | |
1241 db_.GetLastChange(changes); | 1388 db_.GetLastChange(changes); |
1389 if (changes.empty()) | |
1390 { | |
1391 last = db_.GetLastChangeIndex(); | |
1392 hasLast = true; | |
1393 } | |
1394 | |
1242 transaction.Commit(0); | 1395 transaction.Commit(0); |
1243 } | 1396 } |
1244 | 1397 |
1245 FormatLog(target, changes, "Changes", true, 0); | 1398 FormatLog(target, changes, "Changes", true, 0, hasLast, last); |
1246 } | 1399 } |
1247 | 1400 |
1248 | 1401 |
1249 void ServerIndex::LogExportedResource(const std::string& publicId, | 1402 void ServerIndex::LogExportedResource(const std::string& publicId, |
1250 const std::string& remoteModality) | 1403 const std::string& remoteModality) |
1346 { | 1499 { |
1347 boost::mutex::scoped_lock lock(mutex_); | 1500 boost::mutex::scoped_lock lock(mutex_); |
1348 db_.GetExportedResources(exported, done, since, maxResults); | 1501 db_.GetExportedResources(exported, done, since, maxResults); |
1349 } | 1502 } |
1350 | 1503 |
1351 FormatLog(target, exported, "Exports", done, since); | 1504 FormatLog(target, exported, "Exports", done, since, false, -1); |
1352 } | 1505 } |
1353 | 1506 |
1354 | 1507 |
1355 void ServerIndex::GetLastExportedResource(Json::Value& target) | 1508 void ServerIndex::GetLastExportedResource(Json::Value& target) |
1356 { | 1509 { |
1359 { | 1512 { |
1360 boost::mutex::scoped_lock lock(mutex_); | 1513 boost::mutex::scoped_lock lock(mutex_); |
1361 db_.GetLastExportedResource(exported); | 1514 db_.GetLastExportedResource(exported); |
1362 } | 1515 } |
1363 | 1516 |
1364 FormatLog(target, exported, "Exports", true, 0); | 1517 FormatLog(target, exported, "Exports", true, 0, false, -1); |
1365 } | 1518 } |
1366 | 1519 |
1367 | 1520 |
1368 bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize) | 1521 bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize) |
1369 { | 1522 { |
1370 if (maximumStorageSize_ != 0) | 1523 if (maximumStorageSize_ != 0) |
1371 { | 1524 { |
1372 uint64_t currentSize = currentStorageSize_ - listener_->GetSizeOfFilesToRemove(); | 1525 assert(maximumStorageSize_ >= instanceSize); |
1373 assert(db_.GetTotalCompressedSize() == currentSize); | 1526 |
1374 | 1527 if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize)) |
1375 if (currentSize + instanceSize > maximumStorageSize_) | |
1376 { | 1528 { |
1377 return true; | 1529 return true; |
1378 } | 1530 } |
1379 } | 1531 } |
1380 | 1532 |
2028 LogChange(id, ChangeType_NewChildInstance, type, publicId); | 2180 LogChange(id, ChangeType_NewChildInstance, type, publicId); |
2029 } | 2181 } |
2030 | 2182 |
2031 | 2183 |
2032 | 2184 |
2033 void ServerIndex::LookupIdentifierExact(std::list<std::string>& result, | 2185 void ServerIndex::LookupIdentifierExact(std::vector<std::string>& result, |
2034 ResourceType level, | 2186 ResourceType level, |
2035 const DicomTag& tag, | 2187 const DicomTag& tag, |
2036 const std::string& value) | 2188 const std::string& value) |
2037 { | 2189 { |
2038 assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) || | 2190 assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) || |
2041 (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) || | 2193 (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) || |
2042 (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID)); | 2194 (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID)); |
2043 | 2195 |
2044 result.clear(); | 2196 result.clear(); |
2045 | 2197 |
2046 boost::mutex::scoped_lock lock(mutex_); | 2198 DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true); |
2047 | 2199 |
2048 LookupIdentifierQuery query(level); | 2200 std::vector<DatabaseConstraint> query; |
2049 query.AddConstraint(tag, IdentifierConstraintType_Equal, value); | 2201 query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); |
2050 query.Apply(result, db_); | 2202 |
2203 std::list<std::string> tmp; | |
2204 | |
2205 { | |
2206 boost::mutex::scoped_lock lock(mutex_); | |
2207 db_.ApplyLookupResources(tmp, NULL, query, level, 0); | |
2208 } | |
2209 | |
2210 CopyListToVector(result, tmp); | |
2051 } | 2211 } |
2052 | 2212 |
2053 | 2213 |
2054 StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment, | 2214 StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment, |
2055 const std::string& publicId) | 2215 const std::string& publicId) |
2332 | 2492 |
2333 unsigned int ServerIndex::GetDatabaseVersion() | 2493 unsigned int ServerIndex::GetDatabaseVersion() |
2334 { | 2494 { |
2335 boost::mutex::scoped_lock lock(mutex_); | 2495 boost::mutex::scoped_lock lock(mutex_); |
2336 return db_.GetDatabaseVersion(); | 2496 return db_.GetDatabaseVersion(); |
2337 } | |
2338 | |
2339 | |
2340 void ServerIndex::FindCandidates(std::vector<std::string>& resources, | |
2341 std::vector<std::string>& instances, | |
2342 const ::Orthanc::LookupResource& lookup) | |
2343 { | |
2344 boost::mutex::scoped_lock lock(mutex_); | |
2345 | |
2346 std::list<int64_t> tmp; | |
2347 lookup.FindCandidates(tmp, db_); | |
2348 | |
2349 resources.resize(tmp.size()); | |
2350 instances.resize(tmp.size()); | |
2351 | |
2352 size_t pos = 0; | |
2353 for (std::list<int64_t>::const_iterator | |
2354 it = tmp.begin(); it != tmp.end(); ++it, pos++) | |
2355 { | |
2356 assert(db_.GetResourceType(*it) == lookup.GetLevel()); | |
2357 | |
2358 int64_t instance; | |
2359 if (!ServerToolbox::FindOneChildInstance(instance, db_, *it, lookup.GetLevel())) | |
2360 { | |
2361 throw OrthancException(ErrorCode_InternalError); | |
2362 } | |
2363 | |
2364 resources[pos] = db_.GetPublicId(*it); | |
2365 instances[pos] = db_.GetPublicId(instance); | |
2366 } | |
2367 } | 2497 } |
2368 | 2498 |
2369 | 2499 |
2370 bool ServerIndex::LookupParent(std::string& target, | 2500 bool ServerIndex::LookupParent(std::string& target, |
2371 const std::string& publicId, | 2501 const std::string& publicId, |
2430 db_.ClearMainDicomTags(patient); | 2560 db_.ClearMainDicomTags(patient); |
2431 db_.ClearMainDicomTags(study); | 2561 db_.ClearMainDicomTags(study); |
2432 db_.ClearMainDicomTags(series); | 2562 db_.ClearMainDicomTags(series); |
2433 db_.ClearMainDicomTags(instance); | 2563 db_.ClearMainDicomTags(instance); |
2434 | 2564 |
2435 ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, summary); | 2565 { |
2436 ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, summary); | 2566 ResourcesContent content; |
2437 ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, summary); | 2567 content.AddResource(patient, ResourceType_Patient, summary); |
2438 ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, summary); | 2568 content.AddResource(study, ResourceType_Study, summary); |
2569 content.AddResource(series, ResourceType_Series, summary); | |
2570 content.AddResource(instance, ResourceType_Instance, summary); | |
2571 db_.SetResourcesContent(content); | |
2572 } | |
2439 | 2573 |
2440 { | 2574 { |
2441 std::string s; | 2575 std::string s; |
2442 if (dicom.LookupTransferSyntax(s)) | 2576 if (dicom.LookupTransferSyntax(s)) |
2443 { | 2577 { |
2458 catch (OrthancException& e) | 2592 catch (OrthancException& e) |
2459 { | 2593 { |
2460 LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; | 2594 LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; |
2461 } | 2595 } |
2462 } | 2596 } |
2597 | |
2598 | |
2599 void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target, | |
2600 const DatabaseLookup& source, | |
2601 ResourceType queryLevel) const | |
2602 { | |
2603 assert(mainDicomTagsRegistry_.get() != NULL); | |
2604 | |
2605 target.clear(); | |
2606 target.reserve(source.GetConstraintsCount()); | |
2607 | |
2608 for (size_t i = 0; i < source.GetConstraintsCount(); i++) | |
2609 { | |
2610 ResourceType level; | |
2611 DicomTagType type; | |
2612 | |
2613 mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag()); | |
2614 | |
2615 if (type == DicomTagType_Identifier || | |
2616 type == DicomTagType_Main) | |
2617 { | |
2618 // Use the fact that patient-level tags are copied at the study level | |
2619 if (level == ResourceType_Patient && | |
2620 queryLevel != ResourceType_Patient) | |
2621 { | |
2622 level = ResourceType_Study; | |
2623 } | |
2624 | |
2625 target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type)); | |
2626 } | |
2627 } | |
2628 } | |
2629 | |
2630 | |
2631 void ServerIndex::ApplyLookupResources(std::vector<std::string>& resourcesId, | |
2632 std::vector<std::string>* instancesId, | |
2633 const DatabaseLookup& lookup, | |
2634 ResourceType queryLevel, | |
2635 size_t limit) | |
2636 { | |
2637 std::vector<DatabaseConstraint> normalized; | |
2638 NormalizeLookup(normalized, lookup, queryLevel); | |
2639 | |
2640 std::list<std::string> resourcesList, instancesList; | |
2641 | |
2642 { | |
2643 boost::mutex::scoped_lock lock(mutex_); | |
2644 | |
2645 if (instancesId == NULL) | |
2646 { | |
2647 db_.ApplyLookupResources(resourcesList, NULL, normalized, queryLevel, limit); | |
2648 } | |
2649 else | |
2650 { | |
2651 db_.ApplyLookupResources(resourcesList, &instancesList, normalized, queryLevel, limit); | |
2652 } | |
2653 } | |
2654 | |
2655 CopyListToVector(resourcesId, resourcesList); | |
2656 | |
2657 if (instancesId != NULL) | |
2658 { | |
2659 CopyListToVector(*instancesId, instancesList); | |
2660 } | |
2661 } | |
2463 } | 2662 } |