Mercurial > hg > orthanc
comparison OrthancServer/Sources/ServerIndex.cpp @ 4044:d25f4c0fa160 framework
splitting code into OrthancFramework and OrthancServer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 10 Jun 2020 20:30:34 +0200 |
parents | OrthancServer/ServerIndex.cpp@058b5ade8acd |
children | 05b8fd21089c |
comparison
equal
deleted
inserted
replaced
4043:6c6239aec462 | 4044:d25f4c0fa160 |
---|---|
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-2020 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 "ServerIndex.h" | |
36 | |
37 #ifndef NOMINMAX | |
38 #define NOMINMAX | |
39 #endif | |
40 | |
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" | |
49 #include "OrthancConfiguration.h" | |
50 #include "Search/DatabaseLookup.h" | |
51 #include "Search/DicomTagConstraint.h" | |
52 #include "ServerContext.h" | |
53 #include "ServerIndexChange.h" | |
54 #include "ServerToolbox.h" | |
55 | |
56 #include <boost/lexical_cast.hpp> | |
57 #include <stdio.h> | |
58 | |
59 static const uint64_t MEGA_BYTES = 1024 * 1024; | |
60 | |
61 namespace Orthanc | |
62 { | |
63 static void CopyListToVector(std::vector<std::string>& target, | |
64 const std::list<std::string>& source) | |
65 { | |
66 target.resize(source.size()); | |
67 | |
68 size_t pos = 0; | |
69 | |
70 for (std::list<std::string>::const_iterator | |
71 it = source.begin(); it != source.end(); ++it) | |
72 { | |
73 target[pos] = *it; | |
74 pos ++; | |
75 } | |
76 } | |
77 | |
78 | |
79 class ServerIndex::Listener : public IDatabaseListener | |
80 { | |
81 private: | |
82 struct FileToRemove | |
83 { | |
84 private: | |
85 std::string uuid_; | |
86 FileContentType type_; | |
87 | |
88 public: | |
89 FileToRemove(const FileInfo& info) : uuid_(info.GetUuid()), | |
90 type_(info.GetContentType()) | |
91 { | |
92 } | |
93 | |
94 const std::string& GetUuid() const | |
95 { | |
96 return uuid_; | |
97 } | |
98 | |
99 FileContentType GetContentType() const | |
100 { | |
101 return type_; | |
102 } | |
103 }; | |
104 | |
105 ServerContext& context_; | |
106 bool hasRemainingLevel_; | |
107 ResourceType remainingType_; | |
108 std::string remainingPublicId_; | |
109 std::list<FileToRemove> pendingFilesToRemove_; | |
110 std::list<ServerIndexChange> pendingChanges_; | |
111 uint64_t sizeOfFilesToRemove_; | |
112 bool insideTransaction_; | |
113 | |
114 void Reset() | |
115 { | |
116 sizeOfFilesToRemove_ = 0; | |
117 hasRemainingLevel_ = false; | |
118 pendingFilesToRemove_.clear(); | |
119 pendingChanges_.clear(); | |
120 } | |
121 | |
122 public: | |
123 Listener(ServerContext& context) : context_(context), | |
124 insideTransaction_(false) | |
125 { | |
126 Reset(); | |
127 assert(ResourceType_Patient < ResourceType_Study && | |
128 ResourceType_Study < ResourceType_Series && | |
129 ResourceType_Series < ResourceType_Instance); | |
130 } | |
131 | |
132 void StartTransaction() | |
133 { | |
134 Reset(); | |
135 insideTransaction_ = true; | |
136 } | |
137 | |
138 void EndTransaction() | |
139 { | |
140 insideTransaction_ = false; | |
141 } | |
142 | |
143 uint64_t GetSizeOfFilesToRemove() | |
144 { | |
145 return sizeOfFilesToRemove_; | |
146 } | |
147 | |
148 void CommitFilesToRemove() | |
149 { | |
150 for (std::list<FileToRemove>::const_iterator | |
151 it = pendingFilesToRemove_.begin(); | |
152 it != pendingFilesToRemove_.end(); ++it) | |
153 { | |
154 try | |
155 { | |
156 context_.RemoveFile(it->GetUuid(), it->GetContentType()); | |
157 } | |
158 catch (OrthancException& e) | |
159 { | |
160 LOG(ERROR) << "Unable to remove an attachment from the storage area: " | |
161 << it->GetUuid() << " (type: " << EnumerationToString(it->GetContentType()) << ")"; | |
162 } | |
163 } | |
164 } | |
165 | |
166 void CommitChanges() | |
167 { | |
168 for (std::list<ServerIndexChange>::const_iterator | |
169 it = pendingChanges_.begin(); | |
170 it != pendingChanges_.end(); ++it) | |
171 { | |
172 context_.SignalChange(*it); | |
173 } | |
174 } | |
175 | |
176 virtual void SignalRemainingAncestor(ResourceType parentType, | |
177 const std::string& publicId) | |
178 { | |
179 VLOG(1) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")"; | |
180 | |
181 if (hasRemainingLevel_) | |
182 { | |
183 if (parentType < remainingType_) | |
184 { | |
185 remainingType_ = parentType; | |
186 remainingPublicId_ = publicId; | |
187 } | |
188 } | |
189 else | |
190 { | |
191 hasRemainingLevel_ = true; | |
192 remainingType_ = parentType; | |
193 remainingPublicId_ = publicId; | |
194 } | |
195 } | |
196 | |
197 virtual void SignalFileDeleted(const FileInfo& info) | |
198 { | |
199 assert(Toolbox::IsUuid(info.GetUuid())); | |
200 pendingFilesToRemove_.push_back(FileToRemove(info)); | |
201 sizeOfFilesToRemove_ += info.GetCompressedSize(); | |
202 } | |
203 | |
204 virtual void SignalChange(const ServerIndexChange& change) | |
205 { | |
206 VLOG(1) << "Change related to resource " << change.GetPublicId() << " of type " | |
207 << EnumerationToString(change.GetResourceType()) << ": " | |
208 << EnumerationToString(change.GetChangeType()); | |
209 | |
210 if (insideTransaction_) | |
211 { | |
212 pendingChanges_.push_back(change); | |
213 } | |
214 else | |
215 { | |
216 context_.SignalChange(change); | |
217 } | |
218 } | |
219 | |
220 bool HasRemainingLevel() const | |
221 { | |
222 return hasRemainingLevel_; | |
223 } | |
224 | |
225 ResourceType GetRemainingType() const | |
226 { | |
227 assert(HasRemainingLevel()); | |
228 return remainingType_; | |
229 } | |
230 | |
231 const std::string& GetRemainingPublicId() const | |
232 { | |
233 assert(HasRemainingLevel()); | |
234 return remainingPublicId_; | |
235 } | |
236 }; | |
237 | |
238 | |
239 class ServerIndex::Transaction | |
240 { | |
241 private: | |
242 ServerIndex& index_; | |
243 std::unique_ptr<IDatabaseWrapper::ITransaction> transaction_; | |
244 bool isCommitted_; | |
245 | |
246 public: | |
247 Transaction(ServerIndex& index) : | |
248 index_(index), | |
249 isCommitted_(false) | |
250 { | |
251 transaction_.reset(index_.db_.StartTransaction()); | |
252 transaction_->Begin(); | |
253 | |
254 index_.listener_->StartTransaction(); | |
255 } | |
256 | |
257 ~Transaction() | |
258 { | |
259 index_.listener_->EndTransaction(); | |
260 | |
261 if (!isCommitted_) | |
262 { | |
263 transaction_->Rollback(); | |
264 } | |
265 } | |
266 | |
267 void Commit(uint64_t sizeOfAddedFiles) | |
268 { | |
269 if (!isCommitted_) | |
270 { | |
271 int64_t delta = (static_cast<int64_t>(sizeOfAddedFiles) - | |
272 static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove())); | |
273 | |
274 transaction_->Commit(delta); | |
275 | |
276 // We can remove the files once the SQLite transaction has | |
277 // been successfully committed. Some files might have to be | |
278 // deleted because of recycling. | |
279 index_.listener_->CommitFilesToRemove(); | |
280 | |
281 // Send all the pending changes to the Orthanc plugins | |
282 index_.listener_->CommitChanges(); | |
283 | |
284 isCommitted_ = true; | |
285 } | |
286 } | |
287 }; | |
288 | |
289 | |
290 class ServerIndex::UnstableResourcePayload | |
291 { | |
292 private: | |
293 ResourceType type_; | |
294 std::string publicId_; | |
295 boost::posix_time::ptime time_; | |
296 | |
297 public: | |
298 UnstableResourcePayload() : type_(ResourceType_Instance) | |
299 { | |
300 } | |
301 | |
302 UnstableResourcePayload(Orthanc::ResourceType type, | |
303 const std::string& publicId) : | |
304 type_(type), | |
305 publicId_(publicId) | |
306 { | |
307 time_ = boost::posix_time::second_clock::local_time(); | |
308 } | |
309 | |
310 unsigned int GetAge() const | |
311 { | |
312 return (boost::posix_time::second_clock::local_time() - time_).total_seconds(); | |
313 } | |
314 | |
315 ResourceType GetResourceType() const | |
316 { | |
317 return type_; | |
318 } | |
319 | |
320 const std::string& GetPublicId() const | |
321 { | |
322 return publicId_; | |
323 } | |
324 }; | |
325 | |
326 | |
327 class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable | |
328 { | |
329 private: | |
330 class TagInfo | |
331 { | |
332 private: | |
333 ResourceType level_; | |
334 DicomTagType type_; | |
335 | |
336 public: | |
337 TagInfo() | |
338 { | |
339 } | |
340 | |
341 TagInfo(ResourceType level, | |
342 DicomTagType type) : | |
343 level_(level), | |
344 type_(type) | |
345 { | |
346 } | |
347 | |
348 ResourceType GetLevel() const | |
349 { | |
350 return level_; | |
351 } | |
352 | |
353 DicomTagType GetType() const | |
354 { | |
355 return type_; | |
356 } | |
357 }; | |
358 | |
359 typedef std::map<DicomTag, TagInfo> Registry; | |
360 | |
361 | |
362 Registry registry_; | |
363 | |
364 void LoadTags(ResourceType level) | |
365 { | |
366 { | |
367 const DicomTag* tags = NULL; | |
368 size_t size; | |
369 | |
370 ServerToolbox::LoadIdentifiers(tags, size, level); | |
371 | |
372 for (size_t i = 0; i < size; i++) | |
373 { | |
374 if (registry_.find(tags[i]) == registry_.end()) | |
375 { | |
376 registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); | |
377 } | |
378 else | |
379 { | |
380 // These patient-level tags are copied in the study level | |
381 assert(level == ResourceType_Study && | |
382 (tags[i] == DICOM_TAG_PATIENT_ID || | |
383 tags[i] == DICOM_TAG_PATIENT_NAME || | |
384 tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); | |
385 } | |
386 } | |
387 } | |
388 | |
389 { | |
390 std::set<DicomTag> tags; | |
391 DicomMap::GetMainDicomTags(tags, level); | |
392 | |
393 for (std::set<DicomTag>::const_iterator | |
394 tag = tags.begin(); tag != tags.end(); ++tag) | |
395 { | |
396 if (registry_.find(*tag) == registry_.end()) | |
397 { | |
398 registry_[*tag] = TagInfo(level, DicomTagType_Main); | |
399 } | |
400 } | |
401 } | |
402 } | |
403 | |
404 public: | |
405 MainDicomTagsRegistry() | |
406 { | |
407 LoadTags(ResourceType_Patient); | |
408 LoadTags(ResourceType_Study); | |
409 LoadTags(ResourceType_Series); | |
410 LoadTags(ResourceType_Instance); | |
411 } | |
412 | |
413 void LookupTag(ResourceType& level, | |
414 DicomTagType& type, | |
415 const DicomTag& tag) const | |
416 { | |
417 Registry::const_iterator it = registry_.find(tag); | |
418 | |
419 if (it == registry_.end()) | |
420 { | |
421 // Default values | |
422 level = ResourceType_Instance; | |
423 type = DicomTagType_Generic; | |
424 } | |
425 else | |
426 { | |
427 level = it->second.GetLevel(); | |
428 type = it->second.GetType(); | |
429 } | |
430 } | |
431 }; | |
432 | |
433 | |
434 bool ServerIndex::DeleteResource(Json::Value& target, | |
435 const std::string& uuid, | |
436 ResourceType expectedType) | |
437 { | |
438 boost::mutex::scoped_lock lock(mutex_); | |
439 | |
440 Transaction t(*this); | |
441 | |
442 int64_t id; | |
443 ResourceType type; | |
444 if (!db_.LookupResource(id, type, uuid) || | |
445 expectedType != type) | |
446 { | |
447 return false; | |
448 } | |
449 | |
450 db_.DeleteResource(id); | |
451 | |
452 if (listener_->HasRemainingLevel()) | |
453 { | |
454 ResourceType type = listener_->GetRemainingType(); | |
455 const std::string& uuid = listener_->GetRemainingPublicId(); | |
456 | |
457 target["RemainingAncestor"] = Json::Value(Json::objectValue); | |
458 target["RemainingAncestor"]["Path"] = GetBasePath(type, uuid); | |
459 target["RemainingAncestor"]["Type"] = EnumerationToString(type); | |
460 target["RemainingAncestor"]["ID"] = uuid; | |
461 } | |
462 else | |
463 { | |
464 target["RemainingAncestor"] = Json::nullValue; | |
465 } | |
466 | |
467 t.Commit(0); | |
468 | |
469 return true; | |
470 } | |
471 | |
472 | |
473 void ServerIndex::FlushThread(ServerIndex* that, | |
474 unsigned int threadSleep) | |
475 { | |
476 // By default, wait for 10 seconds before flushing | |
477 unsigned int sleep = 10; | |
478 | |
479 try | |
480 { | |
481 boost::mutex::scoped_lock lock(that->mutex_); | |
482 std::string sleepString; | |
483 | |
484 if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) && | |
485 Toolbox::IsInteger(sleepString)) | |
486 { | |
487 sleep = boost::lexical_cast<unsigned int>(sleepString); | |
488 } | |
489 } | |
490 catch (boost::bad_lexical_cast&) | |
491 { | |
492 } | |
493 | |
494 LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")"; | |
495 | |
496 unsigned int count = 0; | |
497 | |
498 while (!that->done_) | |
499 { | |
500 boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep)); | |
501 count++; | |
502 if (count < sleep) | |
503 { | |
504 continue; | |
505 } | |
506 | |
507 Logging::Flush(); | |
508 | |
509 boost::mutex::scoped_lock lock(that->mutex_); | |
510 | |
511 try | |
512 { | |
513 that->db_.FlushToDisk(); | |
514 } | |
515 catch (OrthancException&) | |
516 { | |
517 LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)"; | |
518 } | |
519 | |
520 count = 0; | |
521 } | |
522 | |
523 LOG(INFO) << "Stopping the database flushing thread"; | |
524 } | |
525 | |
526 | |
527 static bool ComputeExpectedNumberOfInstances(int64_t& target, | |
528 const DicomMap& dicomSummary) | |
529 { | |
530 try | |
531 { | |
532 const DicomValue* value; | |
533 const DicomValue* value2; | |
534 | |
535 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL && | |
536 !value->IsNull() && | |
537 !value->IsBinary() && | |
538 (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL && | |
539 !value2->IsNull() && | |
540 !value2->IsBinary()) | |
541 { | |
542 // Patch for series with temporal positions thanks to Will Ryder | |
543 int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent()); | |
544 int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent()); | |
545 target = imagesInAcquisition * countTemporalPositions; | |
546 return (target > 0); | |
547 } | |
548 | |
549 else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL && | |
550 !value->IsNull() && | |
551 !value->IsBinary() && | |
552 (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL && | |
553 !value2->IsBinary() && | |
554 !value2->IsNull()) | |
555 { | |
556 // Support of Cardio-PET images | |
557 int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent()); | |
558 int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent()); | |
559 target = numberOfSlices * numberOfTimeSlices; | |
560 return (target > 0); | |
561 } | |
562 | |
563 else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL && | |
564 !value->IsNull() && | |
565 !value->IsBinary()) | |
566 { | |
567 target = boost::lexical_cast<int64_t>(value->GetContent()); | |
568 return (target > 0); | |
569 } | |
570 } | |
571 catch (OrthancException&) | |
572 { | |
573 } | |
574 catch (boost::bad_lexical_cast&) | |
575 { | |
576 } | |
577 | |
578 return false; | |
579 } | |
580 | |
581 | |
582 | |
583 | |
584 static bool LookupStringMetadata(std::string& result, | |
585 const std::map<MetadataType, std::string>& metadata, | |
586 MetadataType type) | |
587 { | |
588 std::map<MetadataType, std::string>::const_iterator found = metadata.find(type); | |
589 | |
590 if (found == metadata.end()) | |
591 { | |
592 return false; | |
593 } | |
594 else | |
595 { | |
596 result = found->second; | |
597 return true; | |
598 } | |
599 } | |
600 | |
601 | |
602 static bool LookupIntegerMetadata(int64_t& result, | |
603 const std::map<MetadataType, std::string>& metadata, | |
604 MetadataType type) | |
605 { | |
606 std::string s; | |
607 if (!LookupStringMetadata(s, metadata, type)) | |
608 { | |
609 return false; | |
610 } | |
611 | |
612 try | |
613 { | |
614 result = boost::lexical_cast<int64_t>(s); | |
615 return true; | |
616 } | |
617 catch (boost::bad_lexical_cast&) | |
618 { | |
619 return false; | |
620 } | |
621 } | |
622 | |
623 | |
624 void ServerIndex::LogChange(int64_t internalId, | |
625 ChangeType changeType, | |
626 ResourceType resourceType, | |
627 const std::string& publicId) | |
628 { | |
629 ServerIndexChange change(changeType, resourceType, publicId); | |
630 | |
631 if (changeType <= ChangeType_INTERNAL_LastLogged) | |
632 { | |
633 db_.LogChange(internalId, change); | |
634 } | |
635 | |
636 assert(listener_.get() != NULL); | |
637 listener_->SignalChange(change); | |
638 } | |
639 | |
640 | |
641 uint64_t ServerIndex::IncrementGlobalSequenceInternal(GlobalProperty property) | |
642 { | |
643 std::string oldValue; | |
644 | |
645 if (db_.LookupGlobalProperty(oldValue, property)) | |
646 { | |
647 uint64_t oldNumber; | |
648 | |
649 try | |
650 { | |
651 oldNumber = boost::lexical_cast<uint64_t>(oldValue); | |
652 db_.SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1)); | |
653 return oldNumber + 1; | |
654 } | |
655 catch (boost::bad_lexical_cast&) | |
656 { | |
657 throw OrthancException(ErrorCode_InternalError); | |
658 } | |
659 } | |
660 else | |
661 { | |
662 // Initialize the sequence at "1" | |
663 db_.SetGlobalProperty(property, "1"); | |
664 return 1; | |
665 } | |
666 } | |
667 | |
668 | |
669 | |
670 ServerIndex::ServerIndex(ServerContext& context, | |
671 IDatabaseWrapper& db, | |
672 unsigned int threadSleep) : | |
673 done_(false), | |
674 db_(db), | |
675 maximumStorageSize_(0), | |
676 maximumPatients_(0), | |
677 mainDicomTagsRegistry_(new MainDicomTagsRegistry) | |
678 { | |
679 listener_.reset(new Listener(context)); | |
680 db_.SetListener(*listener_); | |
681 | |
682 // Initial recycling if the parameters have changed since the last | |
683 // execution of Orthanc | |
684 StandaloneRecycling(); | |
685 | |
686 if (db.HasFlushToDisk()) | |
687 { | |
688 flushThread_ = boost::thread(FlushThread, this, threadSleep); | |
689 } | |
690 | |
691 unstableResourcesMonitorThread_ = boost::thread | |
692 (UnstableResourcesMonitorThread, this, threadSleep); | |
693 } | |
694 | |
695 | |
696 | |
697 ServerIndex::~ServerIndex() | |
698 { | |
699 if (!done_) | |
700 { | |
701 LOG(ERROR) << "INTERNAL ERROR: ServerIndex::Stop() should be invoked manually to avoid mess in the destruction order!"; | |
702 Stop(); | |
703 } | |
704 } | |
705 | |
706 | |
707 | |
708 void ServerIndex::Stop() | |
709 { | |
710 if (!done_) | |
711 { | |
712 done_ = true; | |
713 | |
714 if (db_.HasFlushToDisk() && | |
715 flushThread_.joinable()) | |
716 { | |
717 flushThread_.join(); | |
718 } | |
719 | |
720 if (unstableResourcesMonitorThread_.joinable()) | |
721 { | |
722 unstableResourcesMonitorThread_.join(); | |
723 } | |
724 } | |
725 } | |
726 | |
727 | |
728 static void SetInstanceMetadata(ResourcesContent& content, | |
729 std::map<MetadataType, std::string>& instanceMetadata, | |
730 int64_t instance, | |
731 MetadataType metadata, | |
732 const std::string& value) | |
733 { | |
734 content.AddMetadata(instance, metadata, value); | |
735 instanceMetadata[metadata] = value; | |
736 } | |
737 | |
738 | |
739 void ServerIndex::SignalNewResource(ChangeType changeType, | |
740 ResourceType level, | |
741 const std::string& publicId, | |
742 int64_t internalId) | |
743 { | |
744 ServerIndexChange change(changeType, level, publicId); | |
745 db_.LogChange(internalId, change); | |
746 | |
747 assert(listener_.get() != NULL); | |
748 listener_->SignalChange(change); | |
749 } | |
750 | |
751 | |
752 StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata, | |
753 DicomInstanceToStore& instanceToStore, | |
754 const Attachments& attachments, | |
755 bool overwrite) | |
756 { | |
757 boost::mutex::scoped_lock lock(mutex_); | |
758 | |
759 const DicomMap& dicomSummary = instanceToStore.GetSummary(); | |
760 const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata(); | |
761 | |
762 int64_t expectedInstances; | |
763 const bool hasExpectedInstances = | |
764 ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary); | |
765 | |
766 instanceMetadata.clear(); | |
767 | |
768 const std::string hashPatient = instanceToStore.GetHasher().HashPatient(); | |
769 const std::string hashStudy = instanceToStore.GetHasher().HashStudy(); | |
770 const std::string hashSeries = instanceToStore.GetHasher().HashSeries(); | |
771 const std::string hashInstance = instanceToStore.GetHasher().HashInstance(); | |
772 | |
773 try | |
774 { | |
775 Transaction t(*this); | |
776 | |
777 IDatabaseWrapper::CreateInstanceResult status; | |
778 int64_t instanceId; | |
779 | |
780 // Check whether this instance is already stored | |
781 if (!db_.CreateInstance(status, instanceId, hashPatient, | |
782 hashStudy, hashSeries, hashInstance)) | |
783 { | |
784 // The instance already exists | |
785 | |
786 if (overwrite) | |
787 { | |
788 // Overwrite the old instance | |
789 LOG(INFO) << "Overwriting instance: " << hashInstance; | |
790 db_.DeleteResource(instanceId); | |
791 | |
792 // Re-create the instance, now that the old one is removed | |
793 if (!db_.CreateInstance(status, instanceId, hashPatient, | |
794 hashStudy, hashSeries, hashInstance)) | |
795 { | |
796 throw OrthancException(ErrorCode_InternalError); | |
797 } | |
798 } | |
799 else | |
800 { | |
801 // Do nothing if the instance already exists and overwriting is disabled | |
802 db_.GetAllMetadata(instanceMetadata, instanceId); | |
803 return StoreStatus_AlreadyStored; | |
804 } | |
805 } | |
806 | |
807 | |
808 // Warn about the creation of new resources. The order must be | |
809 // from instance to patient. | |
810 | |
811 // NB: In theory, could be sped up by grouping the underlying | |
812 // calls to "db_.LogChange()". However, this would only have an | |
813 // impact when new patient/study/series get created, which | |
814 // occurs far less often that creating new instances. The | |
815 // positive impact looks marginal in practice. | |
816 SignalNewResource(ChangeType_NewInstance, ResourceType_Instance, hashInstance, instanceId); | |
817 | |
818 if (status.isNewSeries_) | |
819 { | |
820 SignalNewResource(ChangeType_NewSeries, ResourceType_Series, hashSeries, status.seriesId_); | |
821 } | |
822 | |
823 if (status.isNewStudy_) | |
824 { | |
825 SignalNewResource(ChangeType_NewStudy, ResourceType_Study, hashStudy, status.studyId_); | |
826 } | |
827 | |
828 if (status.isNewPatient_) | |
829 { | |
830 SignalNewResource(ChangeType_NewPatient, ResourceType_Patient, hashPatient, status.patientId_); | |
831 } | |
832 | |
833 | |
834 // Ensure there is enough room in the storage for the new instance | |
835 uint64_t instanceSize = 0; | |
836 for (Attachments::const_iterator it = attachments.begin(); | |
837 it != attachments.end(); ++it) | |
838 { | |
839 instanceSize += it->GetCompressedSize(); | |
840 } | |
841 | |
842 Recycle(instanceSize, hashPatient /* don't consider the current patient for recycling */); | |
843 | |
844 | |
845 // Attach the files to the newly created instance | |
846 for (Attachments::const_iterator it = attachments.begin(); | |
847 it != attachments.end(); ++it) | |
848 { | |
849 db_.AddAttachment(instanceId, *it); | |
850 } | |
851 | |
852 | |
853 { | |
854 ResourcesContent content; | |
855 | |
856 // Populate the tags of the newly-created resources | |
857 | |
858 content.AddResource(instanceId, ResourceType_Instance, dicomSummary); | |
859 | |
860 if (status.isNewSeries_) | |
861 { | |
862 content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary); | |
863 } | |
864 | |
865 if (status.isNewStudy_) | |
866 { | |
867 content.AddResource(status.studyId_, ResourceType_Study, dicomSummary); | |
868 } | |
869 | |
870 if (status.isNewPatient_) | |
871 { | |
872 content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary); | |
873 } | |
874 | |
875 | |
876 // Attach the user-specified metadata | |
877 | |
878 for (MetadataMap::const_iterator | |
879 it = metadata.begin(); it != metadata.end(); ++it) | |
880 { | |
881 switch (it->first.first) | |
882 { | |
883 case ResourceType_Patient: | |
884 content.AddMetadata(status.patientId_, it->first.second, it->second); | |
885 break; | |
886 | |
887 case ResourceType_Study: | |
888 content.AddMetadata(status.studyId_, it->first.second, it->second); | |
889 break; | |
890 | |
891 case ResourceType_Series: | |
892 content.AddMetadata(status.seriesId_, it->first.second, it->second); | |
893 break; | |
894 | |
895 case ResourceType_Instance: | |
896 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
897 it->first.second, it->second); | |
898 break; | |
899 | |
900 default: | |
901 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
902 } | |
903 } | |
904 | |
905 | |
906 // Attach the auto-computed metadata for the patient/study/series levels | |
907 std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */); | |
908 content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now); | |
909 content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now); | |
910 content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now); | |
911 | |
912 if (status.isNewSeries_ && | |
913 hasExpectedInstances) | |
914 { | |
915 content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances, | |
916 boost::lexical_cast<std::string>(expectedInstances)); | |
917 } | |
918 | |
919 | |
920 // Attach the auto-computed metadata for the instance level, | |
921 // reflecting these additions into the input metadata map | |
922 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
923 MetadataType_Instance_ReceptionDate, now); | |
924 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_RemoteAet, | |
925 instanceToStore.GetOrigin().GetRemoteAetC()); | |
926 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin, | |
927 EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin())); | |
928 | |
929 | |
930 { | |
931 std::string s; | |
932 | |
933 if (instanceToStore.LookupTransferSyntax(s)) | |
934 { | |
935 // New in Orthanc 1.2.0 | |
936 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
937 MetadataType_Instance_TransferSyntax, s); | |
938 } | |
939 | |
940 if (instanceToStore.GetOrigin().LookupRemoteIp(s)) | |
941 { | |
942 // New in Orthanc 1.4.0 | |
943 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
944 MetadataType_Instance_RemoteIp, s); | |
945 } | |
946 | |
947 if (instanceToStore.GetOrigin().LookupCalledAet(s)) | |
948 { | |
949 // New in Orthanc 1.4.0 | |
950 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
951 MetadataType_Instance_CalledAet, s); | |
952 } | |
953 | |
954 if (instanceToStore.GetOrigin().LookupHttpUsername(s)) | |
955 { | |
956 // New in Orthanc 1.4.0 | |
957 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
958 MetadataType_Instance_HttpUsername, s); | |
959 } | |
960 } | |
961 | |
962 | |
963 const DicomValue* value; | |
964 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && | |
965 !value->IsNull() && | |
966 !value->IsBinary()) | |
967 { | |
968 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
969 MetadataType_Instance_SopClassUid, value->GetContent()); | |
970 } | |
971 | |
972 | |
973 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || | |
974 (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) | |
975 { | |
976 if (!value->IsNull() && | |
977 !value->IsBinary()) | |
978 { | |
979 SetInstanceMetadata(content, instanceMetadata, instanceId, | |
980 MetadataType_Instance_IndexInSeries, value->GetContent()); | |
981 } | |
982 } | |
983 | |
984 | |
985 db_.SetResourcesContent(content); | |
986 } | |
987 | |
988 | |
989 // Check whether the series of this new instance is now completed | |
990 int64_t expectedNumberOfInstances; | |
991 if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary)) | |
992 { | |
993 SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_, expectedNumberOfInstances); | |
994 if (seriesStatus == SeriesStatus_Complete) | |
995 { | |
996 LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries); | |
997 } | |
998 } | |
999 | |
1000 | |
1001 // Mark the parent resources of this instance as unstable | |
1002 MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries); | |
1003 MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy); | |
1004 MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient); | |
1005 | |
1006 t.Commit(instanceSize); | |
1007 | |
1008 return StoreStatus_Success; | |
1009 } | |
1010 catch (OrthancException& e) | |
1011 { | |
1012 LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; | |
1013 } | |
1014 | |
1015 return StoreStatus_Failure; | |
1016 } | |
1017 | |
1018 | |
1019 void ServerIndex::GetGlobalStatistics(/* out */ uint64_t& diskSize, | |
1020 /* out */ uint64_t& uncompressedSize, | |
1021 /* out */ uint64_t& countPatients, | |
1022 /* out */ uint64_t& countStudies, | |
1023 /* out */ uint64_t& countSeries, | |
1024 /* out */ uint64_t& countInstances) | |
1025 { | |
1026 boost::mutex::scoped_lock lock(mutex_); | |
1027 diskSize = db_.GetTotalCompressedSize(); | |
1028 uncompressedSize = db_.GetTotalUncompressedSize(); | |
1029 countPatients = db_.GetResourceCount(ResourceType_Patient); | |
1030 countStudies = db_.GetResourceCount(ResourceType_Study); | |
1031 countSeries = db_.GetResourceCount(ResourceType_Series); | |
1032 countInstances = db_.GetResourceCount(ResourceType_Instance); | |
1033 } | |
1034 | |
1035 | |
1036 SeriesStatus ServerIndex::GetSeriesStatus(int64_t id, | |
1037 int64_t expectedNumberOfInstances) | |
1038 { | |
1039 std::list<std::string> values; | |
1040 db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries); | |
1041 | |
1042 std::set<int64_t> instances; | |
1043 | |
1044 for (std::list<std::string>::const_iterator | |
1045 it = values.begin(); it != values.end(); ++it) | |
1046 { | |
1047 int64_t index; | |
1048 | |
1049 try | |
1050 { | |
1051 index = boost::lexical_cast<int64_t>(*it); | |
1052 } | |
1053 catch (boost::bad_lexical_cast&) | |
1054 { | |
1055 return SeriesStatus_Unknown; | |
1056 } | |
1057 | |
1058 if (!(index > 0 && index <= expectedNumberOfInstances)) | |
1059 { | |
1060 // Out-of-range instance index | |
1061 return SeriesStatus_Inconsistent; | |
1062 } | |
1063 | |
1064 if (instances.find(index) != instances.end()) | |
1065 { | |
1066 // Twice the same instance index | |
1067 return SeriesStatus_Inconsistent; | |
1068 } | |
1069 | |
1070 instances.insert(index); | |
1071 } | |
1072 | |
1073 if (static_cast<int64_t>(instances.size()) == expectedNumberOfInstances) | |
1074 { | |
1075 return SeriesStatus_Complete; | |
1076 } | |
1077 else | |
1078 { | |
1079 return SeriesStatus_Missing; | |
1080 } | |
1081 } | |
1082 | |
1083 | |
1084 void ServerIndex::MainDicomTagsToJson(Json::Value& target, | |
1085 int64_t resourceId, | |
1086 ResourceType resourceType) | |
1087 { | |
1088 DicomMap tags; | |
1089 db_.GetMainDicomTags(tags, resourceId); | |
1090 | |
1091 if (resourceType == ResourceType_Study) | |
1092 { | |
1093 DicomMap t1, t2; | |
1094 tags.ExtractStudyInformation(t1); | |
1095 tags.ExtractPatientInformation(t2); | |
1096 | |
1097 target["MainDicomTags"] = Json::objectValue; | |
1098 FromDcmtkBridge::ToJson(target["MainDicomTags"], t1, true); | |
1099 | |
1100 target["PatientMainDicomTags"] = Json::objectValue; | |
1101 FromDcmtkBridge::ToJson(target["PatientMainDicomTags"], t2, true); | |
1102 } | |
1103 else | |
1104 { | |
1105 target["MainDicomTags"] = Json::objectValue; | |
1106 FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true); | |
1107 } | |
1108 } | |
1109 | |
1110 | |
1111 bool ServerIndex::LookupResource(Json::Value& result, | |
1112 const std::string& publicId, | |
1113 ResourceType expectedType) | |
1114 { | |
1115 result = Json::objectValue; | |
1116 | |
1117 boost::mutex::scoped_lock lock(mutex_); | |
1118 | |
1119 // Lookup for the requested resource | |
1120 int64_t id; | |
1121 ResourceType type; | |
1122 std::string parent; | |
1123 if (!db_.LookupResourceAndParent(id, type, parent, publicId) || | |
1124 type != expectedType) | |
1125 { | |
1126 return false; | |
1127 } | |
1128 | |
1129 // Set information about the parent resource (if it exists) | |
1130 if (type == ResourceType_Patient) | |
1131 { | |
1132 if (!parent.empty()) | |
1133 { | |
1134 throw OrthancException(ErrorCode_DatabasePlugin); | |
1135 } | |
1136 } | |
1137 else | |
1138 { | |
1139 if (parent.empty()) | |
1140 { | |
1141 throw OrthancException(ErrorCode_DatabasePlugin); | |
1142 } | |
1143 | |
1144 switch (type) | |
1145 { | |
1146 case ResourceType_Study: | |
1147 result["ParentPatient"] = parent; | |
1148 break; | |
1149 | |
1150 case ResourceType_Series: | |
1151 result["ParentStudy"] = parent; | |
1152 break; | |
1153 | |
1154 case ResourceType_Instance: | |
1155 result["ParentSeries"] = parent; | |
1156 break; | |
1157 | |
1158 default: | |
1159 throw OrthancException(ErrorCode_InternalError); | |
1160 } | |
1161 } | |
1162 | |
1163 // List the children resources | |
1164 std::list<std::string> children; | |
1165 db_.GetChildrenPublicId(children, id); | |
1166 | |
1167 if (type != ResourceType_Instance) | |
1168 { | |
1169 Json::Value c = Json::arrayValue; | |
1170 | |
1171 for (std::list<std::string>::const_iterator | |
1172 it = children.begin(); it != children.end(); ++it) | |
1173 { | |
1174 c.append(*it); | |
1175 } | |
1176 | |
1177 switch (type) | |
1178 { | |
1179 case ResourceType_Patient: | |
1180 result["Studies"] = c; | |
1181 break; | |
1182 | |
1183 case ResourceType_Study: | |
1184 result["Series"] = c; | |
1185 break; | |
1186 | |
1187 case ResourceType_Series: | |
1188 result["Instances"] = c; | |
1189 break; | |
1190 | |
1191 default: | |
1192 throw OrthancException(ErrorCode_InternalError); | |
1193 } | |
1194 } | |
1195 | |
1196 // Extract the metadata | |
1197 std::map<MetadataType, std::string> metadata; | |
1198 db_.GetAllMetadata(metadata, id); | |
1199 | |
1200 // Set the resource type | |
1201 switch (type) | |
1202 { | |
1203 case ResourceType_Patient: | |
1204 result["Type"] = "Patient"; | |
1205 break; | |
1206 | |
1207 case ResourceType_Study: | |
1208 result["Type"] = "Study"; | |
1209 break; | |
1210 | |
1211 case ResourceType_Series: | |
1212 { | |
1213 result["Type"] = "Series"; | |
1214 | |
1215 int64_t i; | |
1216 if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances)) | |
1217 { | |
1218 result["ExpectedNumberOfInstances"] = static_cast<int>(i); | |
1219 result["Status"] = EnumerationToString(GetSeriesStatus(id, i)); | |
1220 } | |
1221 else | |
1222 { | |
1223 result["ExpectedNumberOfInstances"] = Json::nullValue; | |
1224 result["Status"] = EnumerationToString(SeriesStatus_Unknown); | |
1225 } | |
1226 | |
1227 break; | |
1228 } | |
1229 | |
1230 case ResourceType_Instance: | |
1231 { | |
1232 result["Type"] = "Instance"; | |
1233 | |
1234 FileInfo attachment; | |
1235 if (!db_.LookupAttachment(attachment, id, FileContentType_Dicom)) | |
1236 { | |
1237 throw OrthancException(ErrorCode_InternalError); | |
1238 } | |
1239 | |
1240 result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize()); | |
1241 result["FileUuid"] = attachment.GetUuid(); | |
1242 | |
1243 int64_t i; | |
1244 if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries)) | |
1245 { | |
1246 result["IndexInSeries"] = static_cast<int>(i); | |
1247 } | |
1248 else | |
1249 { | |
1250 result["IndexInSeries"] = Json::nullValue; | |
1251 } | |
1252 | |
1253 break; | |
1254 } | |
1255 | |
1256 default: | |
1257 throw OrthancException(ErrorCode_InternalError); | |
1258 } | |
1259 | |
1260 // Record the remaining information | |
1261 result["ID"] = publicId; | |
1262 MainDicomTagsToJson(result, id, type); | |
1263 | |
1264 std::string tmp; | |
1265 | |
1266 if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom)) | |
1267 { | |
1268 result["AnonymizedFrom"] = tmp; | |
1269 } | |
1270 | |
1271 if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom)) | |
1272 { | |
1273 result["ModifiedFrom"] = tmp; | |
1274 } | |
1275 | |
1276 if (type == ResourceType_Patient || | |
1277 type == ResourceType_Study || | |
1278 type == ResourceType_Series) | |
1279 { | |
1280 result["IsStable"] = !unstableResources_.Contains(id); | |
1281 | |
1282 if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate)) | |
1283 { | |
1284 result["LastUpdate"] = tmp; | |
1285 } | |
1286 } | |
1287 | |
1288 return true; | |
1289 } | |
1290 | |
1291 | |
1292 bool ServerIndex::LookupAttachment(FileInfo& attachment, | |
1293 const std::string& instanceUuid, | |
1294 FileContentType contentType) | |
1295 { | |
1296 boost::mutex::scoped_lock lock(mutex_); | |
1297 | |
1298 int64_t id; | |
1299 ResourceType type; | |
1300 if (!db_.LookupResource(id, type, instanceUuid)) | |
1301 { | |
1302 throw OrthancException(ErrorCode_UnknownResource); | |
1303 } | |
1304 | |
1305 if (db_.LookupAttachment(attachment, id, contentType)) | |
1306 { | |
1307 assert(attachment.GetContentType() == contentType); | |
1308 return true; | |
1309 } | |
1310 else | |
1311 { | |
1312 return false; | |
1313 } | |
1314 } | |
1315 | |
1316 | |
1317 | |
1318 void ServerIndex::GetAllUuids(std::list<std::string>& target, | |
1319 ResourceType resourceType) | |
1320 { | |
1321 boost::mutex::scoped_lock lock(mutex_); | |
1322 db_.GetAllPublicIds(target, resourceType); | |
1323 } | |
1324 | |
1325 | |
1326 void ServerIndex::GetAllUuids(std::list<std::string>& target, | |
1327 ResourceType resourceType, | |
1328 size_t since, | |
1329 size_t limit) | |
1330 { | |
1331 if (limit == 0) | |
1332 { | |
1333 target.clear(); | |
1334 return; | |
1335 } | |
1336 | |
1337 boost::mutex::scoped_lock lock(mutex_); | |
1338 db_.GetAllPublicIds(target, resourceType, since, limit); | |
1339 } | |
1340 | |
1341 | |
1342 template <typename T> | |
1343 static void FormatLog(Json::Value& target, | |
1344 const std::list<T>& log, | |
1345 const std::string& name, | |
1346 bool done, | |
1347 int64_t since, | |
1348 bool hasLast, | |
1349 int64_t last) | |
1350 { | |
1351 Json::Value items = Json::arrayValue; | |
1352 for (typename std::list<T>::const_iterator | |
1353 it = log.begin(); it != log.end(); ++it) | |
1354 { | |
1355 Json::Value item; | |
1356 it->Format(item); | |
1357 items.append(item); | |
1358 } | |
1359 | |
1360 target = Json::objectValue; | |
1361 target[name] = items; | |
1362 target["Done"] = done; | |
1363 | |
1364 if (!hasLast) | |
1365 { | |
1366 // Best-effort guess of the last index in the sequence | |
1367 if (log.empty()) | |
1368 { | |
1369 last = since; | |
1370 } | |
1371 else | |
1372 { | |
1373 last = log.back().GetSeq(); | |
1374 } | |
1375 } | |
1376 | |
1377 target["Last"] = static_cast<int>(last); | |
1378 } | |
1379 | |
1380 | |
1381 void ServerIndex::GetChanges(Json::Value& target, | |
1382 int64_t since, | |
1383 unsigned int maxResults) | |
1384 { | |
1385 std::list<ServerIndexChange> changes; | |
1386 bool done; | |
1387 bool hasLast = false; | |
1388 int64_t last = 0; | |
1389 | |
1390 { | |
1391 boost::mutex::scoped_lock lock(mutex_); | |
1392 | |
1393 // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as | |
1394 // "GetLastChange()" involves calls to "GetPublicId()" | |
1395 Transaction transaction(*this); | |
1396 | |
1397 db_.GetChanges(changes, done, since, maxResults); | |
1398 if (changes.empty()) | |
1399 { | |
1400 last = db_.GetLastChangeIndex(); | |
1401 hasLast = true; | |
1402 } | |
1403 | |
1404 transaction.Commit(0); | |
1405 } | |
1406 | |
1407 FormatLog(target, changes, "Changes", done, since, hasLast, last); | |
1408 } | |
1409 | |
1410 | |
1411 void ServerIndex::GetLastChange(Json::Value& target) | |
1412 { | |
1413 std::list<ServerIndexChange> changes; | |
1414 bool hasLast = false; | |
1415 int64_t last = 0; | |
1416 | |
1417 { | |
1418 boost::mutex::scoped_lock lock(mutex_); | |
1419 | |
1420 // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as | |
1421 // "GetLastChange()" involves calls to "GetPublicId()" | |
1422 Transaction transaction(*this); | |
1423 | |
1424 db_.GetLastChange(changes); | |
1425 if (changes.empty()) | |
1426 { | |
1427 last = db_.GetLastChangeIndex(); | |
1428 hasLast = true; | |
1429 } | |
1430 | |
1431 transaction.Commit(0); | |
1432 } | |
1433 | |
1434 FormatLog(target, changes, "Changes", true, 0, hasLast, last); | |
1435 } | |
1436 | |
1437 | |
1438 void ServerIndex::LogExportedResource(const std::string& publicId, | |
1439 const std::string& remoteModality) | |
1440 { | |
1441 boost::mutex::scoped_lock lock(mutex_); | |
1442 Transaction transaction(*this); | |
1443 | |
1444 int64_t id; | |
1445 ResourceType type; | |
1446 if (!db_.LookupResource(id, type, publicId)) | |
1447 { | |
1448 throw OrthancException(ErrorCode_InexistentItem); | |
1449 } | |
1450 | |
1451 std::string patientId; | |
1452 std::string studyInstanceUid; | |
1453 std::string seriesInstanceUid; | |
1454 std::string sopInstanceUid; | |
1455 | |
1456 int64_t currentId = id; | |
1457 ResourceType currentType = type; | |
1458 | |
1459 // Iteratively go up inside the patient/study/series/instance hierarchy | |
1460 bool done = false; | |
1461 while (!done) | |
1462 { | |
1463 DicomMap map; | |
1464 db_.GetMainDicomTags(map, currentId); | |
1465 | |
1466 switch (currentType) | |
1467 { | |
1468 case ResourceType_Patient: | |
1469 if (map.HasTag(DICOM_TAG_PATIENT_ID)) | |
1470 { | |
1471 patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent(); | |
1472 } | |
1473 done = true; | |
1474 break; | |
1475 | |
1476 case ResourceType_Study: | |
1477 if (map.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)) | |
1478 { | |
1479 studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent(); | |
1480 } | |
1481 currentType = ResourceType_Patient; | |
1482 break; | |
1483 | |
1484 case ResourceType_Series: | |
1485 if (map.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)) | |
1486 { | |
1487 seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent(); | |
1488 } | |
1489 currentType = ResourceType_Study; | |
1490 break; | |
1491 | |
1492 case ResourceType_Instance: | |
1493 if (map.HasTag(DICOM_TAG_SOP_INSTANCE_UID)) | |
1494 { | |
1495 sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent(); | |
1496 } | |
1497 currentType = ResourceType_Series; | |
1498 break; | |
1499 | |
1500 default: | |
1501 throw OrthancException(ErrorCode_InternalError); | |
1502 } | |
1503 | |
1504 // If we have not reached the Patient level, find the parent of | |
1505 // the current resource | |
1506 if (!done) | |
1507 { | |
1508 bool ok = db_.LookupParent(currentId, currentId); | |
1509 assert(ok); | |
1510 } | |
1511 } | |
1512 | |
1513 ExportedResource resource(-1, | |
1514 type, | |
1515 publicId, | |
1516 remoteModality, | |
1517 SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */), | |
1518 patientId, | |
1519 studyInstanceUid, | |
1520 seriesInstanceUid, | |
1521 sopInstanceUid); | |
1522 | |
1523 db_.LogExportedResource(resource); | |
1524 transaction.Commit(0); | |
1525 } | |
1526 | |
1527 | |
1528 void ServerIndex::GetExportedResources(Json::Value& target, | |
1529 int64_t since, | |
1530 unsigned int maxResults) | |
1531 { | |
1532 std::list<ExportedResource> exported; | |
1533 bool done; | |
1534 | |
1535 { | |
1536 boost::mutex::scoped_lock lock(mutex_); | |
1537 db_.GetExportedResources(exported, done, since, maxResults); | |
1538 } | |
1539 | |
1540 FormatLog(target, exported, "Exports", done, since, false, -1); | |
1541 } | |
1542 | |
1543 | |
1544 void ServerIndex::GetLastExportedResource(Json::Value& target) | |
1545 { | |
1546 std::list<ExportedResource> exported; | |
1547 | |
1548 { | |
1549 boost::mutex::scoped_lock lock(mutex_); | |
1550 db_.GetLastExportedResource(exported); | |
1551 } | |
1552 | |
1553 FormatLog(target, exported, "Exports", true, 0, false, -1); | |
1554 } | |
1555 | |
1556 | |
1557 bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize) | |
1558 { | |
1559 if (maximumStorageSize_ != 0) | |
1560 { | |
1561 assert(maximumStorageSize_ >= instanceSize); | |
1562 | |
1563 if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize)) | |
1564 { | |
1565 return true; | |
1566 } | |
1567 } | |
1568 | |
1569 if (maximumPatients_ != 0) | |
1570 { | |
1571 uint64_t patientCount = db_.GetResourceCount(ResourceType_Patient); | |
1572 if (patientCount > maximumPatients_) | |
1573 { | |
1574 return true; | |
1575 } | |
1576 } | |
1577 | |
1578 return false; | |
1579 } | |
1580 | |
1581 | |
1582 void ServerIndex::Recycle(uint64_t instanceSize, | |
1583 const std::string& newPatientId) | |
1584 { | |
1585 if (!IsRecyclingNeeded(instanceSize)) | |
1586 { | |
1587 return; | |
1588 } | |
1589 | |
1590 // Check whether other DICOM instances from this patient are | |
1591 // already stored | |
1592 int64_t patientToAvoid; | |
1593 ResourceType type; | |
1594 bool hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId); | |
1595 | |
1596 if (hasPatientToAvoid && type != ResourceType_Patient) | |
1597 { | |
1598 throw OrthancException(ErrorCode_InternalError); | |
1599 } | |
1600 | |
1601 // Iteratively select patient to remove until there is enough | |
1602 // space in the DICOM store | |
1603 int64_t patientToRecycle; | |
1604 while (true) | |
1605 { | |
1606 // If other instances of this patient are already in the store, | |
1607 // we must avoid to recycle them | |
1608 bool ok = hasPatientToAvoid ? | |
1609 db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) : | |
1610 db_.SelectPatientToRecycle(patientToRecycle); | |
1611 | |
1612 if (!ok) | |
1613 { | |
1614 throw OrthancException(ErrorCode_FullStorage); | |
1615 } | |
1616 | |
1617 VLOG(1) << "Recycling one patient"; | |
1618 db_.DeleteResource(patientToRecycle); | |
1619 | |
1620 if (!IsRecyclingNeeded(instanceSize)) | |
1621 { | |
1622 // OK, we're done | |
1623 break; | |
1624 } | |
1625 } | |
1626 } | |
1627 | |
1628 void ServerIndex::SetMaximumPatientCount(unsigned int count) | |
1629 { | |
1630 boost::mutex::scoped_lock lock(mutex_); | |
1631 maximumPatients_ = count; | |
1632 | |
1633 if (count == 0) | |
1634 { | |
1635 LOG(WARNING) << "No limit on the number of stored patients"; | |
1636 } | |
1637 else | |
1638 { | |
1639 LOG(WARNING) << "At most " << count << " patients will be stored"; | |
1640 } | |
1641 | |
1642 StandaloneRecycling(); | |
1643 } | |
1644 | |
1645 void ServerIndex::SetMaximumStorageSize(uint64_t size) | |
1646 { | |
1647 boost::mutex::scoped_lock lock(mutex_); | |
1648 maximumStorageSize_ = size; | |
1649 | |
1650 if (size == 0) | |
1651 { | |
1652 LOG(WARNING) << "No limit on the size of the storage area"; | |
1653 } | |
1654 else | |
1655 { | |
1656 LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area"; | |
1657 } | |
1658 | |
1659 StandaloneRecycling(); | |
1660 } | |
1661 | |
1662 | |
1663 void ServerIndex::StandaloneRecycling() | |
1664 { | |
1665 // WARNING: No mutex here, do not include this as a public method | |
1666 Transaction t(*this); | |
1667 Recycle(0, ""); | |
1668 t.Commit(0); | |
1669 } | |
1670 | |
1671 | |
1672 bool ServerIndex::IsProtectedPatient(const std::string& publicId) | |
1673 { | |
1674 boost::mutex::scoped_lock lock(mutex_); | |
1675 | |
1676 // Lookup for the requested resource | |
1677 int64_t id; | |
1678 ResourceType type; | |
1679 if (!db_.LookupResource(id, type, publicId) || | |
1680 type != ResourceType_Patient) | |
1681 { | |
1682 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
1683 } | |
1684 | |
1685 return db_.IsProtectedPatient(id); | |
1686 } | |
1687 | |
1688 | |
1689 void ServerIndex::SetProtectedPatient(const std::string& publicId, | |
1690 bool isProtected) | |
1691 { | |
1692 boost::mutex::scoped_lock lock(mutex_); | |
1693 Transaction transaction(*this); | |
1694 | |
1695 // Lookup for the requested resource | |
1696 int64_t id; | |
1697 ResourceType type; | |
1698 if (!db_.LookupResource(id, type, publicId) || | |
1699 type != ResourceType_Patient) | |
1700 { | |
1701 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
1702 } | |
1703 | |
1704 db_.SetProtectedPatient(id, isProtected); | |
1705 transaction.Commit(0); | |
1706 | |
1707 if (isProtected) | |
1708 LOG(INFO) << "Patient " << publicId << " has been protected"; | |
1709 else | |
1710 LOG(INFO) << "Patient " << publicId << " has been unprotected"; | |
1711 } | |
1712 | |
1713 | |
1714 void ServerIndex::GetChildren(std::list<std::string>& result, | |
1715 const std::string& publicId) | |
1716 { | |
1717 result.clear(); | |
1718 | |
1719 boost::mutex::scoped_lock lock(mutex_); | |
1720 | |
1721 ResourceType type; | |
1722 int64_t resource; | |
1723 if (!db_.LookupResource(resource, type, publicId)) | |
1724 { | |
1725 throw OrthancException(ErrorCode_UnknownResource); | |
1726 } | |
1727 | |
1728 if (type == ResourceType_Instance) | |
1729 { | |
1730 // An instance cannot have a child | |
1731 throw OrthancException(ErrorCode_BadParameterType); | |
1732 } | |
1733 | |
1734 std::list<int64_t> tmp; | |
1735 db_.GetChildrenInternalId(tmp, resource); | |
1736 | |
1737 for (std::list<int64_t>::const_iterator | |
1738 it = tmp.begin(); it != tmp.end(); ++it) | |
1739 { | |
1740 result.push_back(db_.GetPublicId(*it)); | |
1741 } | |
1742 } | |
1743 | |
1744 | |
1745 void ServerIndex::GetChildInstances(std::list<std::string>& result, | |
1746 const std::string& publicId) | |
1747 { | |
1748 result.clear(); | |
1749 | |
1750 boost::mutex::scoped_lock lock(mutex_); | |
1751 | |
1752 ResourceType type; | |
1753 int64_t top; | |
1754 if (!db_.LookupResource(top, type, publicId)) | |
1755 { | |
1756 throw OrthancException(ErrorCode_UnknownResource); | |
1757 } | |
1758 | |
1759 if (type == ResourceType_Instance) | |
1760 { | |
1761 // The resource is already an instance: Do not go down the hierarchy | |
1762 result.push_back(publicId); | |
1763 return; | |
1764 } | |
1765 | |
1766 std::stack<int64_t> toExplore; | |
1767 toExplore.push(top); | |
1768 | |
1769 std::list<int64_t> tmp; | |
1770 | |
1771 while (!toExplore.empty()) | |
1772 { | |
1773 // Get the internal ID of the current resource | |
1774 int64_t resource = toExplore.top(); | |
1775 toExplore.pop(); | |
1776 | |
1777 if (db_.GetResourceType(resource) == ResourceType_Instance) | |
1778 { | |
1779 result.push_back(db_.GetPublicId(resource)); | |
1780 } | |
1781 else | |
1782 { | |
1783 // Tag all the children of this resource as to be explored | |
1784 db_.GetChildrenInternalId(tmp, resource); | |
1785 for (std::list<int64_t>::const_iterator | |
1786 it = tmp.begin(); it != tmp.end(); ++it) | |
1787 { | |
1788 toExplore.push(*it); | |
1789 } | |
1790 } | |
1791 } | |
1792 } | |
1793 | |
1794 | |
1795 void ServerIndex::SetMetadata(const std::string& publicId, | |
1796 MetadataType type, | |
1797 const std::string& value) | |
1798 { | |
1799 boost::mutex::scoped_lock lock(mutex_); | |
1800 Transaction t(*this); | |
1801 | |
1802 ResourceType rtype; | |
1803 int64_t id; | |
1804 if (!db_.LookupResource(id, rtype, publicId)) | |
1805 { | |
1806 throw OrthancException(ErrorCode_UnknownResource); | |
1807 } | |
1808 | |
1809 db_.SetMetadata(id, type, value); | |
1810 | |
1811 if (IsUserMetadata(type)) | |
1812 { | |
1813 LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId); | |
1814 } | |
1815 | |
1816 t.Commit(0); | |
1817 } | |
1818 | |
1819 | |
1820 void ServerIndex::DeleteMetadata(const std::string& publicId, | |
1821 MetadataType type) | |
1822 { | |
1823 boost::mutex::scoped_lock lock(mutex_); | |
1824 Transaction t(*this); | |
1825 | |
1826 ResourceType rtype; | |
1827 int64_t id; | |
1828 if (!db_.LookupResource(id, rtype, publicId)) | |
1829 { | |
1830 throw OrthancException(ErrorCode_UnknownResource); | |
1831 } | |
1832 | |
1833 db_.DeleteMetadata(id, type); | |
1834 | |
1835 if (IsUserMetadata(type)) | |
1836 { | |
1837 LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId); | |
1838 } | |
1839 | |
1840 t.Commit(0); | |
1841 } | |
1842 | |
1843 | |
1844 bool ServerIndex::LookupMetadata(std::string& target, | |
1845 const std::string& publicId, | |
1846 MetadataType type) | |
1847 { | |
1848 boost::mutex::scoped_lock lock(mutex_); | |
1849 | |
1850 ResourceType rtype; | |
1851 int64_t id; | |
1852 if (!db_.LookupResource(id, rtype, publicId)) | |
1853 { | |
1854 throw OrthancException(ErrorCode_UnknownResource); | |
1855 } | |
1856 | |
1857 return db_.LookupMetadata(target, id, type); | |
1858 } | |
1859 | |
1860 | |
1861 void ServerIndex::GetAllMetadata(std::map<MetadataType, std::string>& target, | |
1862 const std::string& publicId) | |
1863 { | |
1864 boost::mutex::scoped_lock lock(mutex_); | |
1865 | |
1866 ResourceType type; | |
1867 int64_t id; | |
1868 if (!db_.LookupResource(id, type, publicId)) | |
1869 { | |
1870 throw OrthancException(ErrorCode_UnknownResource); | |
1871 } | |
1872 | |
1873 return db_.GetAllMetadata(target, id); | |
1874 } | |
1875 | |
1876 | |
1877 void ServerIndex::ListAvailableAttachments(std::list<FileContentType>& target, | |
1878 const std::string& publicId, | |
1879 ResourceType expectedType) | |
1880 { | |
1881 boost::mutex::scoped_lock lock(mutex_); | |
1882 | |
1883 ResourceType type; | |
1884 int64_t id; | |
1885 if (!db_.LookupResource(id, type, publicId) || | |
1886 expectedType != type) | |
1887 { | |
1888 throw OrthancException(ErrorCode_UnknownResource); | |
1889 } | |
1890 | |
1891 db_.ListAvailableAttachments(target, id); | |
1892 } | |
1893 | |
1894 | |
1895 bool ServerIndex::LookupParent(std::string& target, | |
1896 const std::string& publicId) | |
1897 { | |
1898 boost::mutex::scoped_lock lock(mutex_); | |
1899 | |
1900 ResourceType type; | |
1901 int64_t id; | |
1902 if (!db_.LookupResource(id, type, publicId)) | |
1903 { | |
1904 throw OrthancException(ErrorCode_UnknownResource); | |
1905 } | |
1906 | |
1907 int64_t parentId; | |
1908 if (db_.LookupParent(parentId, id)) | |
1909 { | |
1910 target = db_.GetPublicId(parentId); | |
1911 return true; | |
1912 } | |
1913 else | |
1914 { | |
1915 return false; | |
1916 } | |
1917 } | |
1918 | |
1919 | |
1920 uint64_t ServerIndex::IncrementGlobalSequence(GlobalProperty sequence) | |
1921 { | |
1922 boost::mutex::scoped_lock lock(mutex_); | |
1923 Transaction transaction(*this); | |
1924 | |
1925 uint64_t seq = IncrementGlobalSequenceInternal(sequence); | |
1926 transaction.Commit(0); | |
1927 | |
1928 return seq; | |
1929 } | |
1930 | |
1931 | |
1932 | |
1933 void ServerIndex::LogChange(ChangeType changeType, | |
1934 const std::string& publicId) | |
1935 { | |
1936 boost::mutex::scoped_lock lock(mutex_); | |
1937 Transaction transaction(*this); | |
1938 | |
1939 int64_t id; | |
1940 ResourceType type; | |
1941 if (!db_.LookupResource(id, type, publicId)) | |
1942 { | |
1943 throw OrthancException(ErrorCode_UnknownResource); | |
1944 } | |
1945 | |
1946 LogChange(id, changeType, type, publicId); | |
1947 transaction.Commit(0); | |
1948 } | |
1949 | |
1950 | |
1951 void ServerIndex::DeleteChanges() | |
1952 { | |
1953 boost::mutex::scoped_lock lock(mutex_); | |
1954 | |
1955 Transaction transaction(*this); | |
1956 db_.ClearChanges(); | |
1957 transaction.Commit(0); | |
1958 } | |
1959 | |
1960 void ServerIndex::DeleteExportedResources() | |
1961 { | |
1962 boost::mutex::scoped_lock lock(mutex_); | |
1963 | |
1964 Transaction transaction(*this); | |
1965 db_.ClearExportedResources(); | |
1966 transaction.Commit(0); | |
1967 } | |
1968 | |
1969 | |
1970 void ServerIndex::GetResourceStatistics(/* out */ ResourceType& type, | |
1971 /* out */ uint64_t& diskSize, | |
1972 /* out */ uint64_t& uncompressedSize, | |
1973 /* out */ unsigned int& countStudies, | |
1974 /* out */ unsigned int& countSeries, | |
1975 /* out */ unsigned int& countInstances, | |
1976 /* out */ uint64_t& dicomDiskSize, | |
1977 /* out */ uint64_t& dicomUncompressedSize, | |
1978 const std::string& publicId) | |
1979 { | |
1980 boost::mutex::scoped_lock lock(mutex_); | |
1981 | |
1982 int64_t top; | |
1983 if (!db_.LookupResource(top, type, publicId)) | |
1984 { | |
1985 throw OrthancException(ErrorCode_UnknownResource); | |
1986 } | |
1987 | |
1988 std::stack<int64_t> toExplore; | |
1989 toExplore.push(top); | |
1990 | |
1991 countInstances = 0; | |
1992 countSeries = 0; | |
1993 countStudies = 0; | |
1994 diskSize = 0; | |
1995 uncompressedSize = 0; | |
1996 dicomDiskSize = 0; | |
1997 dicomUncompressedSize = 0; | |
1998 | |
1999 while (!toExplore.empty()) | |
2000 { | |
2001 // Get the internal ID of the current resource | |
2002 int64_t resource = toExplore.top(); | |
2003 toExplore.pop(); | |
2004 | |
2005 ResourceType thisType = db_.GetResourceType(resource); | |
2006 | |
2007 std::list<FileContentType> f; | |
2008 db_.ListAvailableAttachments(f, resource); | |
2009 | |
2010 for (std::list<FileContentType>::const_iterator | |
2011 it = f.begin(); it != f.end(); ++it) | |
2012 { | |
2013 FileInfo attachment; | |
2014 if (db_.LookupAttachment(attachment, resource, *it)) | |
2015 { | |
2016 if (attachment.GetContentType() == FileContentType_Dicom) | |
2017 { | |
2018 dicomDiskSize += attachment.GetCompressedSize(); | |
2019 dicomUncompressedSize += attachment.GetUncompressedSize(); | |
2020 } | |
2021 | |
2022 diskSize += attachment.GetCompressedSize(); | |
2023 uncompressedSize += attachment.GetUncompressedSize(); | |
2024 } | |
2025 } | |
2026 | |
2027 if (thisType == ResourceType_Instance) | |
2028 { | |
2029 countInstances++; | |
2030 } | |
2031 else | |
2032 { | |
2033 switch (thisType) | |
2034 { | |
2035 case ResourceType_Study: | |
2036 countStudies++; | |
2037 break; | |
2038 | |
2039 case ResourceType_Series: | |
2040 countSeries++; | |
2041 break; | |
2042 | |
2043 default: | |
2044 break; | |
2045 } | |
2046 | |
2047 // Tag all the children of this resource as to be explored | |
2048 std::list<int64_t> tmp; | |
2049 db_.GetChildrenInternalId(tmp, resource); | |
2050 for (std::list<int64_t>::const_iterator | |
2051 it = tmp.begin(); it != tmp.end(); ++it) | |
2052 { | |
2053 toExplore.push(*it); | |
2054 } | |
2055 } | |
2056 } | |
2057 | |
2058 if (countStudies == 0) | |
2059 { | |
2060 countStudies = 1; | |
2061 } | |
2062 | |
2063 if (countSeries == 0) | |
2064 { | |
2065 countSeries = 1; | |
2066 } | |
2067 } | |
2068 | |
2069 | |
2070 void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that, | |
2071 unsigned int threadSleep) | |
2072 { | |
2073 int stableAge; | |
2074 | |
2075 { | |
2076 OrthancConfiguration::ReaderLock lock; | |
2077 stableAge = lock.GetConfiguration().GetUnsignedIntegerParameter("StableAge", 60); | |
2078 } | |
2079 | |
2080 if (stableAge <= 0) | |
2081 { | |
2082 stableAge = 60; | |
2083 } | |
2084 | |
2085 LOG(INFO) << "Starting the monitor for stable resources (stable age = " << stableAge << ")"; | |
2086 | |
2087 while (!that->done_) | |
2088 { | |
2089 // Check for stable resources each few seconds | |
2090 boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep)); | |
2091 | |
2092 boost::mutex::scoped_lock lock(that->mutex_); | |
2093 | |
2094 while (!that->unstableResources_.IsEmpty() && | |
2095 that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge)) | |
2096 { | |
2097 // This DICOM resource has not received any new instance for | |
2098 // some time. It can be considered as stable. | |
2099 | |
2100 UnstableResourcePayload payload; | |
2101 int64_t id = that->unstableResources_.RemoveOldest(payload); | |
2102 | |
2103 // Ensure that the resource is still existing before logging the change | |
2104 if (that->db_.IsExistingResource(id)) | |
2105 { | |
2106 switch (payload.GetResourceType()) | |
2107 { | |
2108 case ResourceType_Patient: | |
2109 that->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId()); | |
2110 break; | |
2111 | |
2112 case ResourceType_Study: | |
2113 that->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId()); | |
2114 break; | |
2115 | |
2116 case ResourceType_Series: | |
2117 that->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId()); | |
2118 break; | |
2119 | |
2120 default: | |
2121 throw OrthancException(ErrorCode_InternalError); | |
2122 } | |
2123 | |
2124 //LOG(INFO) << "Stable resource: " << EnumerationToString(payload.type_) << " " << id; | |
2125 } | |
2126 } | |
2127 } | |
2128 | |
2129 LOG(INFO) << "Closing the monitor thread for stable resources"; | |
2130 } | |
2131 | |
2132 | |
2133 void ServerIndex::MarkAsUnstable(int64_t id, | |
2134 Orthanc::ResourceType type, | |
2135 const std::string& publicId) | |
2136 { | |
2137 // WARNING: Before calling this method, "mutex_" must be locked. | |
2138 | |
2139 assert(type == Orthanc::ResourceType_Patient || | |
2140 type == Orthanc::ResourceType_Study || | |
2141 type == Orthanc::ResourceType_Series); | |
2142 | |
2143 UnstableResourcePayload payload(type, publicId); | |
2144 unstableResources_.AddOrMakeMostRecent(id, payload); | |
2145 //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id; | |
2146 | |
2147 LogChange(id, ChangeType_NewChildInstance, type, publicId); | |
2148 } | |
2149 | |
2150 | |
2151 | |
2152 void ServerIndex::LookupIdentifierExact(std::vector<std::string>& result, | |
2153 ResourceType level, | |
2154 const DicomTag& tag, | |
2155 const std::string& value) | |
2156 { | |
2157 assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) || | |
2158 (level == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) || | |
2159 (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) || | |
2160 (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) || | |
2161 (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID)); | |
2162 | |
2163 result.clear(); | |
2164 | |
2165 DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true); | |
2166 | |
2167 std::vector<DatabaseConstraint> query; | |
2168 query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); | |
2169 | |
2170 std::list<std::string> tmp; | |
2171 | |
2172 { | |
2173 boost::mutex::scoped_lock lock(mutex_); | |
2174 db_.ApplyLookupResources(tmp, NULL, query, level, 0); | |
2175 } | |
2176 | |
2177 CopyListToVector(result, tmp); | |
2178 } | |
2179 | |
2180 | |
2181 StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment, | |
2182 const std::string& publicId) | |
2183 { | |
2184 boost::mutex::scoped_lock lock(mutex_); | |
2185 | |
2186 Transaction t(*this); | |
2187 | |
2188 ResourceType resourceType; | |
2189 int64_t resourceId; | |
2190 if (!db_.LookupResource(resourceId, resourceType, publicId)) | |
2191 { | |
2192 return StoreStatus_Failure; // Inexistent resource | |
2193 } | |
2194 | |
2195 // Remove possible previous attachment | |
2196 db_.DeleteAttachment(resourceId, attachment.GetContentType()); | |
2197 | |
2198 // Locate the patient of the target resource | |
2199 int64_t patientId = resourceId; | |
2200 for (;;) | |
2201 { | |
2202 int64_t parent; | |
2203 if (db_.LookupParent(parent, patientId)) | |
2204 { | |
2205 // We have not reached the patient level yet | |
2206 patientId = parent; | |
2207 } | |
2208 else | |
2209 { | |
2210 // We have reached the patient level | |
2211 break; | |
2212 } | |
2213 } | |
2214 | |
2215 // Possibly apply the recycling mechanism while preserving this patient | |
2216 assert(db_.GetResourceType(patientId) == ResourceType_Patient); | |
2217 Recycle(attachment.GetCompressedSize(), db_.GetPublicId(patientId)); | |
2218 | |
2219 db_.AddAttachment(resourceId, attachment); | |
2220 | |
2221 if (IsUserContentType(attachment.GetContentType())) | |
2222 { | |
2223 LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId); | |
2224 } | |
2225 | |
2226 t.Commit(attachment.GetCompressedSize()); | |
2227 | |
2228 return StoreStatus_Success; | |
2229 } | |
2230 | |
2231 | |
2232 void ServerIndex::DeleteAttachment(const std::string& publicId, | |
2233 FileContentType type) | |
2234 { | |
2235 boost::mutex::scoped_lock lock(mutex_); | |
2236 Transaction t(*this); | |
2237 | |
2238 ResourceType rtype; | |
2239 int64_t id; | |
2240 if (!db_.LookupResource(id, rtype, publicId)) | |
2241 { | |
2242 throw OrthancException(ErrorCode_UnknownResource); | |
2243 } | |
2244 | |
2245 db_.DeleteAttachment(id, type); | |
2246 | |
2247 if (IsUserContentType(type)) | |
2248 { | |
2249 LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId); | |
2250 } | |
2251 | |
2252 t.Commit(0); | |
2253 } | |
2254 | |
2255 | |
2256 void ServerIndex::SetGlobalProperty(GlobalProperty property, | |
2257 const std::string& value) | |
2258 { | |
2259 boost::mutex::scoped_lock lock(mutex_); | |
2260 | |
2261 Transaction transaction(*this); | |
2262 db_.SetGlobalProperty(property, value); | |
2263 transaction.Commit(0); | |
2264 } | |
2265 | |
2266 | |
2267 bool ServerIndex::LookupGlobalProperty(std::string& value, | |
2268 GlobalProperty property) | |
2269 { | |
2270 boost::mutex::scoped_lock lock(mutex_); | |
2271 return db_.LookupGlobalProperty(value, property); | |
2272 } | |
2273 | |
2274 | |
2275 std::string ServerIndex::GetGlobalProperty(GlobalProperty property, | |
2276 const std::string& defaultValue) | |
2277 { | |
2278 std::string value; | |
2279 | |
2280 if (LookupGlobalProperty(value, property)) | |
2281 { | |
2282 return value; | |
2283 } | |
2284 else | |
2285 { | |
2286 return defaultValue; | |
2287 } | |
2288 } | |
2289 | |
2290 | |
2291 bool ServerIndex::GetMainDicomTags(DicomMap& result, | |
2292 const std::string& publicId, | |
2293 ResourceType expectedType, | |
2294 ResourceType levelOfInterest) | |
2295 { | |
2296 // Yes, the following test could be shortened, but we wish to make it as clear as possible | |
2297 if (!(expectedType == ResourceType_Patient && levelOfInterest == ResourceType_Patient) && | |
2298 !(expectedType == ResourceType_Study && levelOfInterest == ResourceType_Patient) && | |
2299 !(expectedType == ResourceType_Study && levelOfInterest == ResourceType_Study) && | |
2300 !(expectedType == ResourceType_Series && levelOfInterest == ResourceType_Series) && | |
2301 !(expectedType == ResourceType_Instance && levelOfInterest == ResourceType_Instance)) | |
2302 { | |
2303 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
2304 } | |
2305 | |
2306 result.Clear(); | |
2307 | |
2308 boost::mutex::scoped_lock lock(mutex_); | |
2309 | |
2310 // Lookup for the requested resource | |
2311 int64_t id; | |
2312 ResourceType type; | |
2313 if (!db_.LookupResource(id, type, publicId) || | |
2314 type != expectedType) | |
2315 { | |
2316 return false; | |
2317 } | |
2318 | |
2319 if (type == ResourceType_Study) | |
2320 { | |
2321 DicomMap tmp; | |
2322 db_.GetMainDicomTags(tmp, id); | |
2323 | |
2324 switch (levelOfInterest) | |
2325 { | |
2326 case ResourceType_Patient: | |
2327 tmp.ExtractPatientInformation(result); | |
2328 return true; | |
2329 | |
2330 case ResourceType_Study: | |
2331 tmp.ExtractStudyInformation(result); | |
2332 return true; | |
2333 | |
2334 default: | |
2335 throw OrthancException(ErrorCode_InternalError); | |
2336 } | |
2337 } | |
2338 else | |
2339 { | |
2340 db_.GetMainDicomTags(result, id); | |
2341 return true; | |
2342 } | |
2343 } | |
2344 | |
2345 | |
2346 bool ServerIndex::GetAllMainDicomTags(DicomMap& result, | |
2347 const std::string& instancePublicId) | |
2348 { | |
2349 result.Clear(); | |
2350 | |
2351 boost::mutex::scoped_lock lock(mutex_); | |
2352 | |
2353 // Lookup for the requested resource | |
2354 int64_t instance; | |
2355 ResourceType type; | |
2356 if (!db_.LookupResource(instance, type, instancePublicId) || | |
2357 type != ResourceType_Instance) | |
2358 { | |
2359 return false; | |
2360 } | |
2361 else | |
2362 { | |
2363 DicomMap tmp; | |
2364 | |
2365 db_.GetMainDicomTags(tmp, instance); | |
2366 result.Merge(tmp); | |
2367 | |
2368 int64_t series; | |
2369 if (!db_.LookupParent(series, instance)) | |
2370 { | |
2371 throw OrthancException(ErrorCode_InternalError); | |
2372 } | |
2373 | |
2374 tmp.Clear(); | |
2375 db_.GetMainDicomTags(tmp, series); | |
2376 result.Merge(tmp); | |
2377 | |
2378 int64_t study; | |
2379 if (!db_.LookupParent(study, series)) | |
2380 { | |
2381 throw OrthancException(ErrorCode_InternalError); | |
2382 } | |
2383 | |
2384 tmp.Clear(); | |
2385 db_.GetMainDicomTags(tmp, study); | |
2386 result.Merge(tmp); | |
2387 | |
2388 #ifndef NDEBUG | |
2389 { | |
2390 // Sanity test to check that all the main DICOM tags from the | |
2391 // patient level are copied at the study level | |
2392 | |
2393 int64_t patient; | |
2394 if (!db_.LookupParent(patient, study)) | |
2395 { | |
2396 throw OrthancException(ErrorCode_InternalError); | |
2397 } | |
2398 | |
2399 tmp.Clear(); | |
2400 db_.GetMainDicomTags(tmp, study); | |
2401 | |
2402 std::set<DicomTag> patientTags; | |
2403 tmp.GetTags(patientTags); | |
2404 | |
2405 for (std::set<DicomTag>::const_iterator | |
2406 it = patientTags.begin(); it != patientTags.end(); ++it) | |
2407 { | |
2408 assert(result.HasTag(*it)); | |
2409 } | |
2410 } | |
2411 #endif | |
2412 | |
2413 return true; | |
2414 } | |
2415 } | |
2416 | |
2417 | |
2418 bool ServerIndex::LookupResourceType(ResourceType& type, | |
2419 const std::string& publicId) | |
2420 { | |
2421 boost::mutex::scoped_lock lock(mutex_); | |
2422 | |
2423 int64_t id; | |
2424 return db_.LookupResource(id, type, publicId); | |
2425 } | |
2426 | |
2427 | |
2428 unsigned int ServerIndex::GetDatabaseVersion() | |
2429 { | |
2430 boost::mutex::scoped_lock lock(mutex_); | |
2431 return db_.GetDatabaseVersion(); | |
2432 } | |
2433 | |
2434 | |
2435 bool ServerIndex::LookupParent(std::string& target, | |
2436 const std::string& publicId, | |
2437 ResourceType parentType) | |
2438 { | |
2439 boost::mutex::scoped_lock lock(mutex_); | |
2440 | |
2441 ResourceType type; | |
2442 int64_t id; | |
2443 if (!db_.LookupResource(id, type, publicId)) | |
2444 { | |
2445 throw OrthancException(ErrorCode_UnknownResource); | |
2446 } | |
2447 | |
2448 while (type != parentType) | |
2449 { | |
2450 int64_t parentId; | |
2451 | |
2452 if (type == ResourceType_Patient || // Cannot further go up in hierarchy | |
2453 !db_.LookupParent(parentId, id)) | |
2454 { | |
2455 return false; | |
2456 } | |
2457 | |
2458 id = parentId; | |
2459 type = GetParentResourceType(type); | |
2460 } | |
2461 | |
2462 target = db_.GetPublicId(id); | |
2463 return true; | |
2464 } | |
2465 | |
2466 | |
2467 void ServerIndex::ReconstructInstance(ParsedDicomFile& dicom) | |
2468 { | |
2469 DicomMap summary; | |
2470 dicom.ExtractDicomSummary(summary); | |
2471 | |
2472 DicomInstanceHasher hasher(summary); | |
2473 | |
2474 boost::mutex::scoped_lock lock(mutex_); | |
2475 | |
2476 try | |
2477 { | |
2478 Transaction t(*this); | |
2479 | |
2480 int64_t patient = -1, study = -1, series = -1, instance = -1; | |
2481 | |
2482 ResourceType dummy; | |
2483 if (!db_.LookupResource(patient, dummy, hasher.HashPatient()) || | |
2484 !db_.LookupResource(study, dummy, hasher.HashStudy()) || | |
2485 !db_.LookupResource(series, dummy, hasher.HashSeries()) || | |
2486 !db_.LookupResource(instance, dummy, hasher.HashInstance()) || | |
2487 patient == -1 || | |
2488 study == -1 || | |
2489 series == -1 || | |
2490 instance == -1) | |
2491 { | |
2492 throw OrthancException(ErrorCode_InternalError); | |
2493 } | |
2494 | |
2495 db_.ClearMainDicomTags(patient); | |
2496 db_.ClearMainDicomTags(study); | |
2497 db_.ClearMainDicomTags(series); | |
2498 db_.ClearMainDicomTags(instance); | |
2499 | |
2500 { | |
2501 ResourcesContent content; | |
2502 content.AddResource(patient, ResourceType_Patient, summary); | |
2503 content.AddResource(study, ResourceType_Study, summary); | |
2504 content.AddResource(series, ResourceType_Series, summary); | |
2505 content.AddResource(instance, ResourceType_Instance, summary); | |
2506 db_.SetResourcesContent(content); | |
2507 } | |
2508 | |
2509 { | |
2510 std::string s; | |
2511 if (dicom.LookupTransferSyntax(s)) | |
2512 { | |
2513 db_.SetMetadata(instance, MetadataType_Instance_TransferSyntax, s); | |
2514 } | |
2515 } | |
2516 | |
2517 const DicomValue* value; | |
2518 if ((value = summary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && | |
2519 !value->IsNull() && | |
2520 !value->IsBinary()) | |
2521 { | |
2522 db_.SetMetadata(instance, MetadataType_Instance_SopClassUid, value->GetContent()); | |
2523 } | |
2524 | |
2525 t.Commit(0); // No change in the DB size | |
2526 } | |
2527 catch (OrthancException& e) | |
2528 { | |
2529 LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; | |
2530 } | |
2531 } | |
2532 | |
2533 | |
2534 void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target, | |
2535 const DatabaseLookup& source, | |
2536 ResourceType queryLevel) const | |
2537 { | |
2538 assert(mainDicomTagsRegistry_.get() != NULL); | |
2539 | |
2540 target.clear(); | |
2541 target.reserve(source.GetConstraintsCount()); | |
2542 | |
2543 for (size_t i = 0; i < source.GetConstraintsCount(); i++) | |
2544 { | |
2545 ResourceType level; | |
2546 DicomTagType type; | |
2547 | |
2548 mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag()); | |
2549 | |
2550 if (type == DicomTagType_Identifier || | |
2551 type == DicomTagType_Main) | |
2552 { | |
2553 // Use the fact that patient-level tags are copied at the study level | |
2554 if (level == ResourceType_Patient && | |
2555 queryLevel != ResourceType_Patient) | |
2556 { | |
2557 level = ResourceType_Study; | |
2558 } | |
2559 | |
2560 target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type)); | |
2561 } | |
2562 } | |
2563 } | |
2564 | |
2565 | |
2566 void ServerIndex::ApplyLookupResources(std::vector<std::string>& resourcesId, | |
2567 std::vector<std::string>* instancesId, | |
2568 const DatabaseLookup& lookup, | |
2569 ResourceType queryLevel, | |
2570 size_t limit) | |
2571 { | |
2572 std::vector<DatabaseConstraint> normalized; | |
2573 NormalizeLookup(normalized, lookup, queryLevel); | |
2574 | |
2575 std::list<std::string> resourcesList, instancesList; | |
2576 | |
2577 { | |
2578 boost::mutex::scoped_lock lock(mutex_); | |
2579 | |
2580 if (instancesId == NULL) | |
2581 { | |
2582 db_.ApplyLookupResources(resourcesList, NULL, normalized, queryLevel, limit); | |
2583 } | |
2584 else | |
2585 { | |
2586 db_.ApplyLookupResources(resourcesList, &instancesList, normalized, queryLevel, limit); | |
2587 } | |
2588 } | |
2589 | |
2590 CopyListToVector(resourcesId, resourcesList); | |
2591 | |
2592 if (instancesId != NULL) | |
2593 { | |
2594 CopyListToVector(*instancesId, instancesList); | |
2595 } | |
2596 } | |
2597 } |