Mercurial > hg > orthanc
view OrthancServer/Sources/ServerIndex.cpp @ 4558:2f4d7ec9b993 db-changes
cont
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 04 Mar 2021 16:58:35 +0100 |
parents | b6d4b735eb4d |
children | 19b1921aee06 |
line wrap: on
line source
/** * Orthanc - A Lightweight, RESTful DICOM Store * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium * Copyright (C) 2017-2021 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * In addition, as a special exception, the copyright holders of this * program give permission to link the code of its release with the * OpenSSL project's "OpenSSL" library (or with modified versions of it * that use the same license as the "OpenSSL" library), and distribute * the linked executables. You must obey the GNU General Public License * in all respects for all of the code used other than "OpenSSL". If you * modify file(s) with this exception, you may extend this exception to * your version of the file(s), but you are not obligated to do so. If * you do not wish to do so, delete this exception statement from your * version. If you delete this exception statement from all source files * in the program, then also delete it here. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. **/ #include "PrecompiledHeadersServer.h" #include "ServerIndex.h" #ifndef NOMINMAX #define NOMINMAX #endif #include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/Toolbox.h" #include "Database/ResourcesContent.h" #include "OrthancConfiguration.h" #include "Search/DatabaseLookup.h" #include "Search/DicomTagConstraint.h" #include "ServerContext.h" #include "ServerIndexChange.h" #include "ServerToolbox.h" #include <boost/lexical_cast.hpp> #include <boost/tuple/tuple.hpp> #include <stdio.h> #include <stack> static const uint64_t MEGA_BYTES = 1024 * 1024; namespace Orthanc { static void CopyListToVector(std::vector<std::string>& target, const std::list<std::string>& source) { target.resize(source.size()); size_t pos = 0; for (std::list<std::string>::const_iterator it = source.begin(); it != source.end(); ++it) { target[pos] = *it; pos ++; } } class ServerIndex::Listener : public IDatabaseListener { private: struct FileToRemove { private: std::string uuid_; FileContentType type_; public: explicit FileToRemove(const FileInfo& info) : uuid_(info.GetUuid()), type_(info.GetContentType()) { } const std::string& GetUuid() const { return uuid_; } FileContentType GetContentType() const { return type_; } }; ServerContext& context_; bool hasRemainingLevel_; ResourceType remainingType_; std::string remainingPublicId_; std::list<FileToRemove> pendingFilesToRemove_; std::list<ServerIndexChange> pendingChanges_; uint64_t sizeOfFilesToRemove_; bool insideTransaction_; void Reset() { sizeOfFilesToRemove_ = 0; hasRemainingLevel_ = false; pendingFilesToRemove_.clear(); pendingChanges_.clear(); } public: explicit Listener(ServerContext& context) : context_(context), insideTransaction_(false) { Reset(); assert(ResourceType_Patient < ResourceType_Study && ResourceType_Study < ResourceType_Series && ResourceType_Series < ResourceType_Instance); } void StartTransaction() { Reset(); insideTransaction_ = true; } void EndTransaction() { insideTransaction_ = false; } uint64_t GetSizeOfFilesToRemove() { return sizeOfFilesToRemove_; } void CommitFilesToRemove() { for (std::list<FileToRemove>::const_iterator it = pendingFilesToRemove_.begin(); it != pendingFilesToRemove_.end(); ++it) { try { context_.RemoveFile(it->GetUuid(), it->GetContentType()); } catch (OrthancException& e) { LOG(ERROR) << "Unable to remove an attachment from the storage area: " << it->GetUuid() << " (type: " << EnumerationToString(it->GetContentType()) << ")"; } } } void CommitChanges() { for (std::list<ServerIndexChange>::const_iterator it = pendingChanges_.begin(); it != pendingChanges_.end(); ++it) { context_.SignalChange(*it); } } virtual void SignalRemainingAncestor(ResourceType parentType, const std::string& publicId) { LOG(TRACE) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")"; if (hasRemainingLevel_) { if (parentType < remainingType_) { remainingType_ = parentType; remainingPublicId_ = publicId; } } else { hasRemainingLevel_ = true; remainingType_ = parentType; remainingPublicId_ = publicId; } } virtual void SignalFileDeleted(const FileInfo& info) { assert(Toolbox::IsUuid(info.GetUuid())); pendingFilesToRemove_.push_back(FileToRemove(info)); sizeOfFilesToRemove_ += info.GetCompressedSize(); } virtual void SignalChange(const ServerIndexChange& change) { LOG(TRACE) << "Change related to resource " << change.GetPublicId() << " of type " << EnumerationToString(change.GetResourceType()) << ": " << EnumerationToString(change.GetChangeType()); if (insideTransaction_) { pendingChanges_.push_back(change); } else { context_.SignalChange(change); } } bool HasRemainingLevel() const { return hasRemainingLevel_; } ResourceType GetRemainingType() const { assert(HasRemainingLevel()); return remainingType_; } const std::string& GetRemainingPublicId() const { assert(HasRemainingLevel()); return remainingPublicId_; } }; class ServerIndex::Transaction { private: ServerIndex& index_; std::unique_ptr<IDatabaseWrapper::ITransaction> transaction_; bool isCommitted_; public: explicit Transaction(ServerIndex& index) : index_(index), isCommitted_(false) { transaction_.reset(index_.db_.StartTransaction()); index_.listener_->StartTransaction(); } ~Transaction() { index_.listener_->EndTransaction(); if (!isCommitted_) { transaction_->Rollback(); } } void Commit(uint64_t sizeOfAddedFiles) { if (!isCommitted_) { int64_t delta = (static_cast<int64_t>(sizeOfAddedFiles) - static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove())); transaction_->Commit(delta); // We can remove the files once the SQLite transaction has // been successfully committed. Some files might have to be // deleted because of recycling. index_.listener_->CommitFilesToRemove(); // Send all the pending changes to the Orthanc plugins index_.listener_->CommitChanges(); isCommitted_ = true; } } }; class ServerIndex::UnstableResourcePayload { private: ResourceType type_; std::string publicId_; boost::posix_time::ptime time_; public: UnstableResourcePayload() : type_(ResourceType_Instance) { } UnstableResourcePayload(Orthanc::ResourceType type, const std::string& publicId) : type_(type), publicId_(publicId), time_(boost::posix_time::second_clock::local_time()) { } unsigned int GetAge() const { return (boost::posix_time::second_clock::local_time() - time_).total_seconds(); } ResourceType GetResourceType() const { return type_; } const std::string& GetPublicId() const { return publicId_; } }; class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable { private: class TagInfo { private: ResourceType level_; DicomTagType type_; public: TagInfo() { } TagInfo(ResourceType level, DicomTagType type) : level_(level), type_(type) { } ResourceType GetLevel() const { return level_; } DicomTagType GetType() const { return type_; } }; typedef std::map<DicomTag, TagInfo> Registry; Registry registry_; void LoadTags(ResourceType level) { { const DicomTag* tags = NULL; size_t size; ServerToolbox::LoadIdentifiers(tags, size, level); for (size_t i = 0; i < size; i++) { if (registry_.find(tags[i]) == registry_.end()) { registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); } else { // These patient-level tags are copied in the study level assert(level == ResourceType_Study && (tags[i] == DICOM_TAG_PATIENT_ID || tags[i] == DICOM_TAG_PATIENT_NAME || tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); } } } { std::set<DicomTag> tags; DicomMap::GetMainDicomTags(tags, level); for (std::set<DicomTag>::const_iterator tag = tags.begin(); tag != tags.end(); ++tag) { if (registry_.find(*tag) == registry_.end()) { registry_[*tag] = TagInfo(level, DicomTagType_Main); } } } } public: MainDicomTagsRegistry() { LoadTags(ResourceType_Patient); LoadTags(ResourceType_Study); LoadTags(ResourceType_Series); LoadTags(ResourceType_Instance); } void LookupTag(ResourceType& level, DicomTagType& type, const DicomTag& tag) const { Registry::const_iterator it = registry_.find(tag); if (it == registry_.end()) { // Default values level = ResourceType_Instance; type = DicomTagType_Generic; } else { level = it->second.GetLevel(); type = it->second.GetType(); } } }; bool ServerIndex::DeleteResource(Json::Value& target, const std::string& uuid, ResourceType expectedType) { boost::mutex::scoped_lock lock(mutex_); Transaction t(*this); int64_t id; ResourceType type; if (!db_.LookupResource(id, type, uuid) || expectedType != type) { return false; } db_.DeleteResource(id); if (listener_->HasRemainingLevel()) { ResourceType remainingType = listener_->GetRemainingType(); const std::string& remainingUuid = listener_->GetRemainingPublicId(); target["RemainingAncestor"] = Json::Value(Json::objectValue); target["RemainingAncestor"]["Path"] = GetBasePath(remainingType, remainingUuid); target["RemainingAncestor"]["Type"] = EnumerationToString(remainingType); target["RemainingAncestor"]["ID"] = remainingUuid; } else { target["RemainingAncestor"] = Json::nullValue; } t.Commit(0); return true; } void ServerIndex::FlushThread(ServerIndex* that, unsigned int threadSleep) { // By default, wait for 10 seconds before flushing unsigned int sleep = 10; try { boost::mutex::scoped_lock lock(that->mutex_); std::string sleepString; if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) && Toolbox::IsInteger(sleepString)) { sleep = boost::lexical_cast<unsigned int>(sleepString); } } catch (boost::bad_lexical_cast&) { } LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")"; unsigned int count = 0; while (!that->done_) { boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep)); count++; if (count < sleep) { continue; } Logging::Flush(); boost::mutex::scoped_lock lock(that->mutex_); try { that->db_.FlushToDisk(); } catch (OrthancException&) { LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)"; } count = 0; } LOG(INFO) << "Stopping the database flushing thread"; } static bool ComputeExpectedNumberOfInstances(int64_t& target, const DicomMap& dicomSummary) { try { const DicomValue* value; const DicomValue* value2; if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL && !value->IsNull() && !value->IsBinary() && (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL && !value2->IsNull() && !value2->IsBinary()) { // Patch for series with temporal positions thanks to Will Ryder int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent()); int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent()); target = imagesInAcquisition * countTemporalPositions; return (target > 0); } else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL && !value->IsNull() && !value->IsBinary() && (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL && !value2->IsBinary() && !value2->IsNull()) { // Support of Cardio-PET images int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent()); int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent()); target = numberOfSlices * numberOfTimeSlices; return (target > 0); } else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL && !value->IsNull() && !value->IsBinary()) { target = boost::lexical_cast<int64_t>(value->GetContent()); return (target > 0); } } catch (OrthancException&) { } catch (boost::bad_lexical_cast&) { } return false; } static bool LookupStringMetadata(std::string& result, const std::map<MetadataType, std::string>& metadata, MetadataType type) { std::map<MetadataType, std::string>::const_iterator found = metadata.find(type); if (found == metadata.end()) { return false; } else { result = found->second; return true; } } static bool LookupIntegerMetadata(int64_t& result, const std::map<MetadataType, std::string>& metadata, MetadataType type) { std::string s; if (!LookupStringMetadata(s, metadata, type)) { return false; } try { result = boost::lexical_cast<int64_t>(s); return true; } catch (boost::bad_lexical_cast&) { return false; } } void ServerIndex::LogChange(int64_t internalId, ChangeType changeType, ResourceType resourceType, const std::string& publicId) { ServerIndexChange change(changeType, resourceType, publicId); if (changeType <= ChangeType_INTERNAL_LastLogged) { db_.LogChange(internalId, change); } assert(listener_.get() != NULL); listener_->SignalChange(change); } uint64_t ServerIndex::IncrementGlobalSequenceInternal(GlobalProperty property) { std::string oldValue; if (db_.LookupGlobalProperty(oldValue, property)) { uint64_t oldNumber; try { oldNumber = boost::lexical_cast<uint64_t>(oldValue); } catch (boost::bad_lexical_cast&) { LOG(ERROR) << "Cannot read the global sequence " << boost::lexical_cast<std::string>(property) << ", resetting it"; oldNumber = 0; } db_.SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1)); return oldNumber + 1; } else { // Initialize the sequence at "1" db_.SetGlobalProperty(property, "1"); return 1; } } bool ServerIndex::IsUnstableResource(int64_t id) { return unstableResources_.Contains(id); } ServerIndex::ServerIndex(ServerContext& context, IDatabaseWrapper& db, unsigned int threadSleep) : done_(false), db_(db), maximumStorageSize_(0), maximumPatients_(0), mainDicomTagsRegistry_(new MainDicomTagsRegistry), maxRetries_(10) { listener_.reset(new Listener(context)); db_.SetListener(*listener_); // Initial recycling if the parameters have changed since the last // execution of Orthanc StandaloneRecycling(); if (db.HasFlushToDisk()) { flushThread_ = boost::thread(FlushThread, this, threadSleep); } unstableResourcesMonitorThread_ = boost::thread (UnstableResourcesMonitorThread, this, threadSleep); } ServerIndex::~ServerIndex() { if (!done_) { LOG(ERROR) << "INTERNAL ERROR: ServerIndex::Stop() should be invoked manually to avoid mess in the destruction order!"; Stop(); } } void ServerIndex::Stop() { if (!done_) { done_ = true; if (db_.HasFlushToDisk() && flushThread_.joinable()) { flushThread_.join(); } if (unstableResourcesMonitorThread_.joinable()) { unstableResourcesMonitorThread_.join(); } } } static void SetInstanceMetadata(ResourcesContent& content, std::map<MetadataType, std::string>& instanceMetadata, int64_t instance, MetadataType metadata, const std::string& value) { content.AddMetadata(instance, metadata, value); instanceMetadata[metadata] = value; } void ServerIndex::SignalNewResource(ChangeType changeType, ResourceType level, const std::string& publicId, int64_t internalId) { ServerIndexChange change(changeType, level, publicId); db_.LogChange(internalId, change); assert(listener_.get() != NULL); listener_->SignalChange(change); } StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata, const DicomMap& dicomSummary, const Attachments& attachments, const MetadataMap& metadata, const DicomInstanceOrigin& origin, bool overwrite, bool hasTransferSyntax, DicomTransferSyntax transferSyntax, bool hasPixelDataOffset, uint64_t pixelDataOffset) { boost::mutex::scoped_lock lock(mutex_); int64_t expectedInstances; const bool hasExpectedInstances = ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary); instanceMetadata.clear(); DicomInstanceHasher hasher(dicomSummary); const std::string hashPatient = hasher.HashPatient(); const std::string hashStudy = hasher.HashStudy(); const std::string hashSeries = hasher.HashSeries(); const std::string hashInstance = hasher.HashInstance(); try { Transaction t(*this); IDatabaseWrapper::CreateInstanceResult status; int64_t instanceId; // Check whether this instance is already stored if (!db_.CreateInstance(status, instanceId, hashPatient, hashStudy, hashSeries, hashInstance)) { // The instance already exists if (overwrite) { // Overwrite the old instance LOG(INFO) << "Overwriting instance: " << hashInstance; db_.DeleteResource(instanceId); // Re-create the instance, now that the old one is removed if (!db_.CreateInstance(status, instanceId, hashPatient, hashStudy, hashSeries, hashInstance)) { throw OrthancException(ErrorCode_InternalError); } } else { // Do nothing if the instance already exists and overwriting is disabled db_.GetAllMetadata(instanceMetadata, instanceId); return StoreStatus_AlreadyStored; } } // Warn about the creation of new resources. The order must be // from instance to patient. // NB: In theory, could be sped up by grouping the underlying // calls to "db_.LogChange()". However, this would only have an // impact when new patient/study/series get created, which // occurs far less often that creating new instances. The // positive impact looks marginal in practice. SignalNewResource(ChangeType_NewInstance, ResourceType_Instance, hashInstance, instanceId); if (status.isNewSeries_) { SignalNewResource(ChangeType_NewSeries, ResourceType_Series, hashSeries, status.seriesId_); } if (status.isNewStudy_) { SignalNewResource(ChangeType_NewStudy, ResourceType_Study, hashStudy, status.studyId_); } if (status.isNewPatient_) { SignalNewResource(ChangeType_NewPatient, ResourceType_Patient, hashPatient, status.patientId_); } // Ensure there is enough room in the storage for the new instance uint64_t instanceSize = 0; for (Attachments::const_iterator it = attachments.begin(); it != attachments.end(); ++it) { instanceSize += it->GetCompressedSize(); } Recycle(instanceSize, hashPatient /* don't consider the current patient for recycling */); // Attach the files to the newly created instance for (Attachments::const_iterator it = attachments.begin(); it != attachments.end(); ++it) { db_.AddAttachment(instanceId, *it); } { ResourcesContent content; // Populate the tags of the newly-created resources content.AddResource(instanceId, ResourceType_Instance, dicomSummary); if (status.isNewSeries_) { content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary); } if (status.isNewStudy_) { content.AddResource(status.studyId_, ResourceType_Study, dicomSummary); } if (status.isNewPatient_) { content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary); } // Attach the user-specified metadata for (MetadataMap::const_iterator it = metadata.begin(); it != metadata.end(); ++it) { switch (it->first.first) { case ResourceType_Patient: content.AddMetadata(status.patientId_, it->first.second, it->second); break; case ResourceType_Study: content.AddMetadata(status.studyId_, it->first.second, it->second); break; case ResourceType_Series: content.AddMetadata(status.seriesId_, it->first.second, it->second); break; case ResourceType_Instance: SetInstanceMetadata(content, instanceMetadata, instanceId, it->first.second, it->second); break; default: throw OrthancException(ErrorCode_ParameterOutOfRange); } } // Attach the auto-computed metadata for the patient/study/series levels std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */); content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now); content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now); content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now); if (status.isNewSeries_) { if (hasExpectedInstances) { content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances, boost::lexical_cast<std::string>(expectedInstances)); } // New in Orthanc 1.9.0 content.AddMetadata(status.seriesId_, MetadataType_RemoteAet, origin.GetRemoteAetC()); } // Attach the auto-computed metadata for the instance level, // reflecting these additions into the input metadata map SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_ReceptionDate, now); SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_RemoteAet, origin.GetRemoteAetC()); SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin, EnumerationToString(origin.GetRequestOrigin())); if (hasTransferSyntax) { // New in Orthanc 1.2.0 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax)); } { std::string s; if (origin.LookupRemoteIp(s)) { // New in Orthanc 1.4.0 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_RemoteIp, s); } if (origin.LookupCalledAet(s)) { // New in Orthanc 1.4.0 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_CalledAet, s); } if (origin.LookupHttpUsername(s)) { // New in Orthanc 1.4.0 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_HttpUsername, s); } } if (hasPixelDataOffset) { // New in Orthanc 1.9.1 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_PixelDataOffset, boost::lexical_cast<std::string>(pixelDataOffset)); } const DicomValue* value; if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && !value->IsNull() && !value->IsBinary()) { SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_SopClassUid, value->GetContent()); } if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) { if (!value->IsNull() && !value->IsBinary()) { SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent())); } } db_.SetResourcesContent(content); } // Check whether the series of this new instance is now completed int64_t expectedNumberOfInstances; if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary)) { SeriesStatus seriesStatus = GetSeriesStatus(db_, status.seriesId_, expectedNumberOfInstances); if (seriesStatus == SeriesStatus_Complete) { LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries); } } // Mark the parent resources of this instance as unstable MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries); MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy); MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient); t.Commit(instanceSize); return StoreStatus_Success; } catch (OrthancException& e) { LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; } return StoreStatus_Failure; } SeriesStatus ServerIndex::GetSeriesStatus(IDatabaseWrapper& db, int64_t id, int64_t expectedNumberOfInstances) { std::list<std::string> values; db.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries); std::set<int64_t> instances; for (std::list<std::string>::const_iterator it = values.begin(); it != values.end(); ++it) { int64_t index; try { index = boost::lexical_cast<int64_t>(*it); } catch (boost::bad_lexical_cast&) { return SeriesStatus_Unknown; } if (!(index > 0 && index <= expectedNumberOfInstances)) { // Out-of-range instance index return SeriesStatus_Inconsistent; } if (instances.find(index) != instances.end()) { // Twice the same instance index return SeriesStatus_Inconsistent; } instances.insert(index); } if (static_cast<int64_t>(instances.size()) == expectedNumberOfInstances) { return SeriesStatus_Complete; } else { return SeriesStatus_Missing; } } void ServerIndex::MainDicomTagsToJson(Json::Value& target, IDatabaseWrapper& db, int64_t resourceId, ResourceType resourceType) { DicomMap tags; db.GetMainDicomTags(tags, resourceId); if (resourceType == ResourceType_Study) { DicomMap t1, t2; tags.ExtractStudyInformation(t1); tags.ExtractPatientInformation(t2); target["MainDicomTags"] = Json::objectValue; FromDcmtkBridge::ToJson(target["MainDicomTags"], t1, true); target["PatientMainDicomTags"] = Json::objectValue; FromDcmtkBridge::ToJson(target["PatientMainDicomTags"], t2, true); } else { target["MainDicomTags"] = Json::objectValue; FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true); } } template <typename T> static void FormatLog(Json::Value& target, const std::list<T>& log, const std::string& name, bool done, int64_t since, bool hasLast, int64_t last) { Json::Value items = Json::arrayValue; for (typename std::list<T>::const_iterator it = log.begin(); it != log.end(); ++it) { Json::Value item; it->Format(item); items.append(item); } target = Json::objectValue; target[name] = items; target["Done"] = done; if (!hasLast) { // Best-effort guess of the last index in the sequence if (log.empty()) { last = since; } else { last = log.back().GetSeq(); } } target["Last"] = static_cast<int>(last); } void ServerIndex::LogExportedResource(const std::string& publicId, const std::string& remoteModality) { boost::mutex::scoped_lock lock(mutex_); Transaction transaction(*this); int64_t id; ResourceType type; if (!db_.LookupResource(id, type, publicId)) { throw OrthancException(ErrorCode_InexistentItem); } std::string patientId; std::string studyInstanceUid; std::string seriesInstanceUid; std::string sopInstanceUid; int64_t currentId = id; ResourceType currentType = type; // Iteratively go up inside the patient/study/series/instance hierarchy bool done = false; while (!done) { DicomMap map; db_.GetMainDicomTags(map, currentId); switch (currentType) { case ResourceType_Patient: if (map.HasTag(DICOM_TAG_PATIENT_ID)) { patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent(); } done = true; break; case ResourceType_Study: if (map.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)) { studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent(); } currentType = ResourceType_Patient; break; case ResourceType_Series: if (map.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)) { seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent(); } currentType = ResourceType_Study; break; case ResourceType_Instance: if (map.HasTag(DICOM_TAG_SOP_INSTANCE_UID)) { sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent(); } currentType = ResourceType_Series; break; default: throw OrthancException(ErrorCode_InternalError); } // If we have not reached the Patient level, find the parent of // the current resource if (!done) { bool ok = db_.LookupParent(currentId, currentId); (void) ok; // Remove warning about unused variable in release builds assert(ok); } } ExportedResource resource(-1, type, publicId, remoteModality, SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */), patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); db_.LogExportedResource(resource); transaction.Commit(0); } bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize) { if (maximumStorageSize_ != 0) { assert(maximumStorageSize_ >= instanceSize); if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize)) { return true; } } if (maximumPatients_ != 0) { uint64_t patientCount = db_.GetResourceCount(ResourceType_Patient); if (patientCount > maximumPatients_) { return true; } } return false; } void ServerIndex::Recycle(uint64_t instanceSize, const std::string& newPatientId) { if (!IsRecyclingNeeded(instanceSize)) { return; } // Check whether other DICOM instances from this patient are // already stored int64_t patientToAvoid; ResourceType type; bool hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId); if (hasPatientToAvoid && type != ResourceType_Patient) { throw OrthancException(ErrorCode_InternalError); } // Iteratively select patient to remove until there is enough // space in the DICOM store int64_t patientToRecycle; while (true) { // If other instances of this patient are already in the store, // we must avoid to recycle them bool ok = hasPatientToAvoid ? db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) : db_.SelectPatientToRecycle(patientToRecycle); if (!ok) { throw OrthancException(ErrorCode_FullStorage); } LOG(TRACE) << "Recycling one patient"; db_.DeleteResource(patientToRecycle); if (!IsRecyclingNeeded(instanceSize)) { // OK, we're done break; } } } void ServerIndex::SetMaximumPatientCount(unsigned int count) { boost::mutex::scoped_lock lock(mutex_); maximumPatients_ = count; if (count == 0) { LOG(WARNING) << "No limit on the number of stored patients"; } else { LOG(WARNING) << "At most " << count << " patients will be stored"; } StandaloneRecycling(); } void ServerIndex::SetMaximumStorageSize(uint64_t size) { boost::mutex::scoped_lock lock(mutex_); maximumStorageSize_ = size; if (size == 0) { LOG(WARNING) << "No limit on the size of the storage area"; } else { LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area"; } StandaloneRecycling(); } void ServerIndex::StandaloneRecycling() { // WARNING: No mutex here, do not include this as a public method Transaction t(*this); Recycle(0, ""); t.Commit(0); } bool ServerIndex::IsProtectedPatient(const std::string& publicId) { boost::mutex::scoped_lock lock(mutex_); // Lookup for the requested resource int64_t id; ResourceType type; if (!db_.LookupResource(id, type, publicId) || type != ResourceType_Patient) { throw OrthancException(ErrorCode_ParameterOutOfRange); } return db_.IsProtectedPatient(id); } void ServerIndex::SetProtectedPatient(const std::string& publicId, bool isProtected) { boost::mutex::scoped_lock lock(mutex_); Transaction transaction(*this); // Lookup for the requested resource int64_t id; ResourceType type; if (!db_.LookupResource(id, type, publicId) || type != ResourceType_Patient) { throw OrthancException(ErrorCode_ParameterOutOfRange); } db_.SetProtectedPatient(id, isProtected); transaction.Commit(0); if (isProtected) LOG(INFO) << "Patient " << publicId << " has been protected"; else LOG(INFO) << "Patient " << publicId << " has been unprotected"; } void ServerIndex::GetChildren(std::list<std::string>& result, const std::string& publicId) { result.clear(); boost::mutex::scoped_lock lock(mutex_); ResourceType type; int64_t resource; if (!db_.LookupResource(resource, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } if (type == ResourceType_Instance) { // An instance cannot have a child throw OrthancException(ErrorCode_BadParameterType); } std::list<int64_t> tmp; db_.GetChildrenInternalId(tmp, resource); for (std::list<int64_t>::const_iterator it = tmp.begin(); it != tmp.end(); ++it) { result.push_back(db_.GetPublicId(*it)); } } void ServerIndex::GetChildInstances(std::list<std::string>& result, const std::string& publicId) { result.clear(); boost::mutex::scoped_lock lock(mutex_); ResourceType type; int64_t top; if (!db_.LookupResource(top, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } if (type == ResourceType_Instance) { // The resource is already an instance: Do not go down the hierarchy result.push_back(publicId); return; } std::stack<int64_t> toExplore; toExplore.push(top); std::list<int64_t> tmp; while (!toExplore.empty()) { // Get the internal ID of the current resource int64_t resource = toExplore.top(); toExplore.pop(); if (db_.GetResourceType(resource) == ResourceType_Instance) { result.push_back(db_.GetPublicId(resource)); } else { // Tag all the children of this resource as to be explored db_.GetChildrenInternalId(tmp, resource); for (std::list<int64_t>::const_iterator it = tmp.begin(); it != tmp.end(); ++it) { toExplore.push(*it); } } } } void ServerIndex::SetMetadata(const std::string& publicId, MetadataType type, const std::string& value) { boost::mutex::scoped_lock lock(mutex_); Transaction t(*this); ResourceType rtype; int64_t id; if (!db_.LookupResource(id, rtype, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } db_.SetMetadata(id, type, value); if (IsUserMetadata(type)) { LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId); } t.Commit(0); } void ServerIndex::DeleteMetadata(const std::string& publicId, MetadataType type) { boost::mutex::scoped_lock lock(mutex_); Transaction t(*this); ResourceType rtype; int64_t id; if (!db_.LookupResource(id, rtype, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } db_.DeleteMetadata(id, type); if (IsUserMetadata(type)) { LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId); } t.Commit(0); } bool ServerIndex::LookupMetadata(std::string& target, const std::string& publicId, ResourceType expectedType, MetadataType type) { boost::mutex::scoped_lock lock(mutex_); ResourceType rtype; int64_t id; if (!db_.LookupResource(id, rtype, publicId) || rtype != expectedType) { throw OrthancException(ErrorCode_UnknownResource); } return db_.LookupMetadata(target, id, type); } void ServerIndex::ListAvailableAttachments(std::set<FileContentType>& target, const std::string& publicId, ResourceType expectedType) { boost::mutex::scoped_lock lock(mutex_); ResourceType type; int64_t id; if (!db_.LookupResource(id, type, publicId) || expectedType != type) { throw OrthancException(ErrorCode_UnknownResource); } db_.ListAvailableAttachments(target, id); } bool ServerIndex::LookupParent(std::string& target, const std::string& publicId) { boost::mutex::scoped_lock lock(mutex_); ResourceType type; int64_t id; if (!db_.LookupResource(id, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } int64_t parentId; if (db_.LookupParent(parentId, id)) { target = db_.GetPublicId(parentId); return true; } else { return false; } } uint64_t ServerIndex::IncrementGlobalSequence(GlobalProperty sequence) { boost::mutex::scoped_lock lock(mutex_); Transaction transaction(*this); uint64_t seq = IncrementGlobalSequenceInternal(sequence); transaction.Commit(0); return seq; } void ServerIndex::LogChange(ChangeType changeType, const std::string& publicId) { boost::mutex::scoped_lock lock(mutex_); Transaction transaction(*this); int64_t id; ResourceType type; if (!db_.LookupResource(id, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } LogChange(id, changeType, type, publicId); transaction.Commit(0); } void ServerIndex::DeleteChanges() { boost::mutex::scoped_lock lock(mutex_); Transaction transaction(*this); db_.ClearChanges(); transaction.Commit(0); } void ServerIndex::DeleteExportedResources() { boost::mutex::scoped_lock lock(mutex_); Transaction transaction(*this); db_.ClearExportedResources(); transaction.Commit(0); } void ServerIndex::GetResourceStatistics(/* out */ ResourceType& type, /* out */ uint64_t& diskSize, /* out */ uint64_t& uncompressedSize, /* out */ unsigned int& countStudies, /* out */ unsigned int& countSeries, /* out */ unsigned int& countInstances, /* out */ uint64_t& dicomDiskSize, /* out */ uint64_t& dicomUncompressedSize, const std::string& publicId) { boost::mutex::scoped_lock lock(mutex_); int64_t top; if (!db_.LookupResource(top, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } std::stack<int64_t> toExplore; toExplore.push(top); countInstances = 0; countSeries = 0; countStudies = 0; diskSize = 0; uncompressedSize = 0; dicomDiskSize = 0; dicomUncompressedSize = 0; while (!toExplore.empty()) { // Get the internal ID of the current resource int64_t resource = toExplore.top(); toExplore.pop(); ResourceType thisType = db_.GetResourceType(resource); std::set<FileContentType> f; db_.ListAvailableAttachments(f, resource); for (std::set<FileContentType>::const_iterator it = f.begin(); it != f.end(); ++it) { FileInfo attachment; if (db_.LookupAttachment(attachment, resource, *it)) { if (attachment.GetContentType() == FileContentType_Dicom) { dicomDiskSize += attachment.GetCompressedSize(); dicomUncompressedSize += attachment.GetUncompressedSize(); } diskSize += attachment.GetCompressedSize(); uncompressedSize += attachment.GetUncompressedSize(); } } if (thisType == ResourceType_Instance) { countInstances++; } else { switch (thisType) { case ResourceType_Study: countStudies++; break; case ResourceType_Series: countSeries++; break; default: break; } // Tag all the children of this resource as to be explored std::list<int64_t> tmp; db_.GetChildrenInternalId(tmp, resource); for (std::list<int64_t>::const_iterator it = tmp.begin(); it != tmp.end(); ++it) { toExplore.push(*it); } } } if (countStudies == 0) { countStudies = 1; } if (countSeries == 0) { countSeries = 1; } } void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that, unsigned int threadSleep) { int stableAge; { OrthancConfiguration::ReaderLock lock; stableAge = lock.GetConfiguration().GetUnsignedIntegerParameter("StableAge", 60); } if (stableAge <= 0) { stableAge = 60; } LOG(INFO) << "Starting the monitor for stable resources (stable age = " << stableAge << ")"; while (!that->done_) { // Check for stable resources each few seconds boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep)); boost::mutex::scoped_lock lock(that->mutex_); while (!that->unstableResources_.IsEmpty() && that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge)) { // This DICOM resource has not received any new instance for // some time. It can be considered as stable. UnstableResourcePayload payload; int64_t id = that->unstableResources_.RemoveOldest(payload); // Ensure that the resource is still existing before logging the change if (that->db_.IsExistingResource(id)) { switch (payload.GetResourceType()) { case ResourceType_Patient: that->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId()); break; case ResourceType_Study: that->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId()); break; case ResourceType_Series: that->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId()); break; default: throw OrthancException(ErrorCode_InternalError); } //LOG(INFO) << "Stable resource: " << EnumerationToString(payload.type_) << " " << id; } } } LOG(INFO) << "Closing the monitor thread for stable resources"; } void ServerIndex::MarkAsUnstable(int64_t id, Orthanc::ResourceType type, const std::string& publicId) { // WARNING: Before calling this method, "mutex_" must be locked. assert(type == Orthanc::ResourceType_Patient || type == Orthanc::ResourceType_Study || type == Orthanc::ResourceType_Series); UnstableResourcePayload payload(type, publicId); unstableResources_.AddOrMakeMostRecent(id, payload); //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id; LogChange(id, ChangeType_NewChildInstance, type, publicId); } void ServerIndex::LookupIdentifierExact(std::vector<std::string>& result, ResourceType level, const DicomTag& tag, const std::string& value) { assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) || (level == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) || (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) || (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) || (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID)); result.clear(); DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true); std::vector<DatabaseConstraint> query; query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); std::list<std::string> tmp; { boost::mutex::scoped_lock lock(mutex_); db_.ApplyLookupResources(tmp, NULL, query, level, 0); } CopyListToVector(result, tmp); } StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment, const std::string& publicId) { boost::mutex::scoped_lock lock(mutex_); Transaction t(*this); ResourceType resourceType; int64_t resourceId; if (!db_.LookupResource(resourceId, resourceType, publicId)) { return StoreStatus_Failure; // Inexistent resource } // Remove possible previous attachment db_.DeleteAttachment(resourceId, attachment.GetContentType()); // Locate the patient of the target resource int64_t patientId = resourceId; for (;;) { int64_t parent; if (db_.LookupParent(parent, patientId)) { // We have not reached the patient level yet patientId = parent; } else { // We have reached the patient level break; } } // Possibly apply the recycling mechanism while preserving this patient assert(db_.GetResourceType(patientId) == ResourceType_Patient); Recycle(attachment.GetCompressedSize(), db_.GetPublicId(patientId)); db_.AddAttachment(resourceId, attachment); if (IsUserContentType(attachment.GetContentType())) { LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId); } t.Commit(attachment.GetCompressedSize()); return StoreStatus_Success; } void ServerIndex::DeleteAttachment(const std::string& publicId, FileContentType type) { boost::mutex::scoped_lock lock(mutex_); Transaction t(*this); ResourceType rtype; int64_t id; if (!db_.LookupResource(id, rtype, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } db_.DeleteAttachment(id, type); if (IsUserContentType(type)) { LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId); } t.Commit(0); } void ServerIndex::SetGlobalProperty(GlobalProperty property, const std::string& value) { boost::mutex::scoped_lock lock(mutex_); Transaction transaction(*this); db_.SetGlobalProperty(property, value); transaction.Commit(0); } bool ServerIndex::LookupGlobalProperty(std::string& value, GlobalProperty property) { boost::mutex::scoped_lock lock(mutex_); return db_.LookupGlobalProperty(value, property); } std::string ServerIndex::GetGlobalProperty(GlobalProperty property, const std::string& defaultValue) { std::string value; if (LookupGlobalProperty(value, property)) { return value; } else { return defaultValue; } } bool ServerIndex::GetMainDicomTags(DicomMap& result, const std::string& publicId, ResourceType expectedType, ResourceType levelOfInterest) { // Yes, the following test could be shortened, but we wish to make it as clear as possible if (!(expectedType == ResourceType_Patient && levelOfInterest == ResourceType_Patient) && !(expectedType == ResourceType_Study && levelOfInterest == ResourceType_Patient) && !(expectedType == ResourceType_Study && levelOfInterest == ResourceType_Study) && !(expectedType == ResourceType_Series && levelOfInterest == ResourceType_Series) && !(expectedType == ResourceType_Instance && levelOfInterest == ResourceType_Instance)) { throw OrthancException(ErrorCode_ParameterOutOfRange); } result.Clear(); boost::mutex::scoped_lock lock(mutex_); // Lookup for the requested resource int64_t id; ResourceType type; if (!db_.LookupResource(id, type, publicId) || type != expectedType) { return false; } if (type == ResourceType_Study) { DicomMap tmp; db_.GetMainDicomTags(tmp, id); switch (levelOfInterest) { case ResourceType_Patient: tmp.ExtractPatientInformation(result); return true; case ResourceType_Study: tmp.ExtractStudyInformation(result); return true; default: throw OrthancException(ErrorCode_InternalError); } } else { db_.GetMainDicomTags(result, id); return true; } } bool ServerIndex::GetAllMainDicomTags(DicomMap& result, const std::string& instancePublicId) { result.Clear(); boost::mutex::scoped_lock lock(mutex_); // Lookup for the requested resource int64_t instance; ResourceType type; if (!db_.LookupResource(instance, type, instancePublicId) || type != ResourceType_Instance) { return false; } else { DicomMap tmp; db_.GetMainDicomTags(tmp, instance); result.Merge(tmp); int64_t series; if (!db_.LookupParent(series, instance)) { throw OrthancException(ErrorCode_InternalError); } tmp.Clear(); db_.GetMainDicomTags(tmp, series); result.Merge(tmp); int64_t study; if (!db_.LookupParent(study, series)) { throw OrthancException(ErrorCode_InternalError); } tmp.Clear(); db_.GetMainDicomTags(tmp, study); result.Merge(tmp); #ifndef NDEBUG { // Sanity test to check that all the main DICOM tags from the // patient level are copied at the study level int64_t patient; if (!db_.LookupParent(patient, study)) { throw OrthancException(ErrorCode_InternalError); } tmp.Clear(); db_.GetMainDicomTags(tmp, study); std::set<DicomTag> patientTags; tmp.GetTags(patientTags); for (std::set<DicomTag>::const_iterator it = patientTags.begin(); it != patientTags.end(); ++it) { assert(result.HasTag(*it)); } } #endif return true; } } bool ServerIndex::LookupResourceType(ResourceType& type, const std::string& publicId) { boost::mutex::scoped_lock lock(mutex_); int64_t id; return db_.LookupResource(id, type, publicId); } unsigned int ServerIndex::GetDatabaseVersion() { boost::mutex::scoped_lock lock(mutex_); return db_.GetDatabaseVersion(); } bool ServerIndex::LookupParent(std::string& target, const std::string& publicId, ResourceType parentType) { boost::mutex::scoped_lock lock(mutex_); ResourceType type; int64_t id; if (!db_.LookupResource(id, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } while (type != parentType) { int64_t parentId; if (type == ResourceType_Patient || // Cannot further go up in hierarchy !db_.LookupParent(parentId, id)) { return false; } id = parentId; type = GetParentResourceType(type); } target = db_.GetPublicId(id); return true; } void ServerIndex::ReconstructInstance(const ParsedDicomFile& dicom) { DicomMap summary; OrthancConfiguration::DefaultExtractDicomSummary(summary, dicom); DicomInstanceHasher hasher(summary); boost::mutex::scoped_lock lock(mutex_); try { Transaction t(*this); int64_t patient = -1, study = -1, series = -1, instance = -1; ResourceType dummy; if (!db_.LookupResource(patient, dummy, hasher.HashPatient()) || !db_.LookupResource(study, dummy, hasher.HashStudy()) || !db_.LookupResource(series, dummy, hasher.HashSeries()) || !db_.LookupResource(instance, dummy, hasher.HashInstance()) || patient == -1 || study == -1 || series == -1 || instance == -1) { throw OrthancException(ErrorCode_InternalError); } db_.ClearMainDicomTags(patient); db_.ClearMainDicomTags(study); db_.ClearMainDicomTags(series); db_.ClearMainDicomTags(instance); { ResourcesContent content; content.AddResource(patient, ResourceType_Patient, summary); content.AddResource(study, ResourceType_Study, summary); content.AddResource(series, ResourceType_Series, summary); content.AddResource(instance, ResourceType_Instance, summary); db_.SetResourcesContent(content); } { DicomTransferSyntax s; if (dicom.LookupTransferSyntax(s)) { db_.SetMetadata(instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(s)); } } const DicomValue* value; if ((value = summary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && !value->IsNull() && !value->IsBinary()) { db_.SetMetadata(instance, MetadataType_Instance_SopClassUid, value->GetContent()); } t.Commit(0); // No change in the DB size } catch (OrthancException& e) { LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; } } void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target, const DatabaseLookup& source, ResourceType queryLevel) const { assert(mainDicomTagsRegistry_.get() != NULL); target.clear(); target.reserve(source.GetConstraintsCount()); for (size_t i = 0; i < source.GetConstraintsCount(); i++) { ResourceType level; DicomTagType type; mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag()); if (type == DicomTagType_Identifier || type == DicomTagType_Main) { // Use the fact that patient-level tags are copied at the study level if (level == ResourceType_Patient && queryLevel != ResourceType_Patient) { level = ResourceType_Study; } target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type)); } } } void ServerIndex::ApplyLookupResources(std::vector<std::string>& resourcesId, std::vector<std::string>* instancesId, const DatabaseLookup& lookup, ResourceType queryLevel, size_t limit) { std::vector<DatabaseConstraint> normalized; NormalizeLookup(normalized, lookup, queryLevel); std::list<std::string> resourcesList, instancesList; { boost::mutex::scoped_lock lock(mutex_); if (instancesId == NULL) { db_.ApplyLookupResources(resourcesList, NULL, normalized, queryLevel, limit); } else { db_.ApplyLookupResources(resourcesList, &instancesList, normalized, queryLevel, limit); } } CopyListToVector(resourcesId, resourcesList); if (instancesId != NULL) { CopyListToVector(*instancesId, instancesList); } } /*** ** PROTOTYPING FOR DB REFACTORING BELOW ***/ namespace { /** * Some handy templates to reduce the verbosity in the definitions * of the internal classes. **/ template <typename Operations, typename Tuple> class TupleOperationsWrapper : public ServerIndex::IReadOnlyOperations { protected: Operations& operations_; const Tuple& tuple_; public: TupleOperationsWrapper(Operations& operations, const Tuple& tuple) : operations_(operations), tuple_(tuple) { } virtual void Apply(ServerIndex::ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE { operations_.ApplyTuple(transaction, tuple_); } }; template <typename T1> class ReadOnlyOperationsT1 : public boost::noncopyable { public: typedef typename boost::tuple<T1> Tuple; virtual ~ReadOnlyOperationsT1() { } virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction, const Tuple& tuple) = 0; void Apply(ServerIndex& index, T1 t1) { const Tuple tuple(t1); TupleOperationsWrapper<ReadOnlyOperationsT1, Tuple> wrapper(*this, tuple); index.Apply(wrapper); } }; template <typename T1, typename T2> class ReadOnlyOperationsT2 : public boost::noncopyable { public: typedef typename boost::tuple<T1, T2> Tuple; virtual ~ReadOnlyOperationsT2() { } virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction, const Tuple& tuple) = 0; void Apply(ServerIndex& index, T1 t1, T2 t2) { const Tuple tuple(t1, t2); TupleOperationsWrapper<ReadOnlyOperationsT2, Tuple> wrapper(*this, tuple); index.Apply(wrapper); } }; template <typename T1, typename T2, typename T3> class ReadOnlyOperationsT3 : public boost::noncopyable { public: typedef typename boost::tuple<T1, T2, T3> Tuple; virtual ~ReadOnlyOperationsT3() { } virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction, const Tuple& tuple) = 0; void Apply(ServerIndex& index, T1 t1, T2 t2, T3 t3) { const Tuple tuple(t1, t2, t3); TupleOperationsWrapper<ReadOnlyOperationsT3, Tuple> wrapper(*this, tuple); index.Apply(wrapper); } }; template <typename T1, typename T2, typename T3, typename T4> class ReadOnlyOperationsT4 : public boost::noncopyable { public: typedef typename boost::tuple<T1, T2, T3, T4> Tuple; virtual ~ReadOnlyOperationsT4() { } virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction, const Tuple& tuple) = 0; void Apply(ServerIndex& index, T1 t1, T2 t2, T3 t3, T4 t4) { const Tuple tuple(t1, t2, t3, t4); TupleOperationsWrapper<ReadOnlyOperationsT4, Tuple> wrapper(*this, tuple); index.Apply(wrapper); } }; template <typename T1, typename T2, typename T3, typename T4, typename T5> class ReadOnlyOperationsT5 : public boost::noncopyable { public: typedef typename boost::tuple<T1, T2, T3, T4, T5> Tuple; virtual ~ReadOnlyOperationsT5() { } virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction, const Tuple& tuple) = 0; void Apply(ServerIndex& index, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5) { const Tuple tuple(t1, t2, t3, t4, t5); TupleOperationsWrapper<ReadOnlyOperationsT5, Tuple> wrapper(*this, tuple); index.Apply(wrapper); } }; template <typename T1, typename T2, typename T3, typename T4, typename T5, typename T6> class ReadOnlyOperationsT6 : public boost::noncopyable { public: typedef typename boost::tuple<T1, T2, T3, T4, T5, T6> Tuple; virtual ~ReadOnlyOperationsT6() { } virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction, const Tuple& tuple) = 0; void Apply(ServerIndex& index, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6) { const Tuple tuple(t1, t2, t3, t4, t5, t6); TupleOperationsWrapper<ReadOnlyOperationsT6, Tuple> wrapper(*this, tuple); index.Apply(wrapper); } }; } class ServerIndex::ReadOnlyWrapper : public IReadOnlyOperations { private: ReadOnlyFunction func_; public: explicit ReadOnlyWrapper(ReadOnlyFunction func) : func_(func) { assert(func_ != NULL); } virtual void Apply(ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE { func_(transaction); } }; class ServerIndex::ReadWriteWrapper : public IReadWriteOperations { private: ReadWriteFunction func_; public: explicit ReadWriteWrapper(ReadWriteFunction func) : func_(func) { assert(func_ != NULL); } virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE { func_(transaction); } }; void ServerIndex::ApplyInternal(IReadOnlyOperations* readOperations, IReadWriteOperations* writeOperations) { if ((readOperations == NULL && writeOperations == NULL) || (readOperations != NULL && writeOperations != NULL)) { throw OrthancException(ErrorCode_InternalError); } unsigned int count = 0; for (;;) { try { boost::mutex::scoped_lock lock(mutex_); // TODO - REMOVE Transaction transaction(*this); // TODO - Only if "TransactionType_SingleStatement" if (readOperations != NULL) { ReadOnlyTransaction transaction(db_); readOperations->Apply(transaction); } else { assert(writeOperations != NULL); ReadWriteTransaction transaction(db_); writeOperations->Apply(transaction); } transaction.Commit(0); return; // Success } catch (OrthancException& e) { if (e.GetErrorCode() == ErrorCode_DatabaseCannotSerialize) { if (count == maxRetries_) { throw; } else { count++; boost::this_thread::sleep(boost::posix_time::milliseconds(100 * count)); } } else if (e.GetErrorCode() == ErrorCode_DatabaseUnavailable) { if (count == maxRetries_) { throw; } else { count++; boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); } } else { throw; } } } } void ServerIndex::Apply(IReadOnlyOperations& operations) { ApplyInternal(&operations, NULL); } void ServerIndex::Apply(IReadWriteOperations& operations) { ApplyInternal(NULL, &operations); } void ServerIndex::Apply(ReadOnlyFunction func) { ReadOnlyWrapper wrapper(func); Apply(wrapper); } void ServerIndex::Apply(ReadWriteFunction func) { ReadWriteWrapper wrapper(func); Apply(wrapper); } bool ServerIndex::ExpandResource(Json::Value& target, const std::string& publicId, ResourceType level) { class Operations : public ReadOnlyOperationsT3<Json::Value*, std::string, ResourceType> { private: ServerIndex& index_; bool found_; public: Operations(ServerIndex& index) : index_(index), found_(false) { } bool HasFound() const { return found_; } virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { Json::Value& target = *tuple.get<0>(); // Lookup for the requested resource int64_t internalId; // unused ResourceType type; std::string parent; if (!transaction.LookupResourceAndParent(internalId, type, parent, tuple.get<1>()) || type != tuple.get<2>()) { found_ = false; } else { target = Json::objectValue; // Set information about the parent resource (if it exists) if (type == ResourceType_Patient) { if (!parent.empty()) { throw OrthancException(ErrorCode_DatabasePlugin); } } else { if (parent.empty()) { throw OrthancException(ErrorCode_DatabasePlugin); } switch (type) { case ResourceType_Study: target["ParentPatient"] = parent; break; case ResourceType_Series: target["ParentStudy"] = parent; break; case ResourceType_Instance: target["ParentSeries"] = parent; break; default: throw OrthancException(ErrorCode_InternalError); } } // List the children resources std::list<std::string> children; transaction.GetChildrenPublicId(children, internalId); if (type != ResourceType_Instance) { Json::Value c = Json::arrayValue; for (std::list<std::string>::const_iterator it = children.begin(); it != children.end(); ++it) { c.append(*it); } switch (type) { case ResourceType_Patient: target["Studies"] = c; break; case ResourceType_Study: target["Series"] = c; break; case ResourceType_Series: target["Instances"] = c; break; default: throw OrthancException(ErrorCode_InternalError); } } // Extract the metadata std::map<MetadataType, std::string> metadata; transaction.GetAllMetadata(metadata, internalId); // Set the resource type switch (type) { case ResourceType_Patient: target["Type"] = "Patient"; break; case ResourceType_Study: target["Type"] = "Study"; break; case ResourceType_Series: { target["Type"] = "Series"; int64_t i; if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances)) { target["ExpectedNumberOfInstances"] = static_cast<int>(i); target["Status"] = EnumerationToString(transaction.GetSeriesStatus(internalId, i)); } else { target["ExpectedNumberOfInstances"] = Json::nullValue; target["Status"] = EnumerationToString(SeriesStatus_Unknown); } break; } case ResourceType_Instance: { target["Type"] = "Instance"; FileInfo attachment; if (!transaction.LookupAttachment(attachment, internalId, FileContentType_Dicom)) { throw OrthancException(ErrorCode_InternalError); } target["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize()); target["FileUuid"] = attachment.GetUuid(); int64_t i; if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries)) { target["IndexInSeries"] = static_cast<int>(i); } else { target["IndexInSeries"] = Json::nullValue; } break; } default: throw OrthancException(ErrorCode_InternalError); } // Record the remaining information target["ID"] = tuple.get<1>(); transaction.MainDicomTagsToJson(target, internalId, type); std::string tmp; if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom)) { target["AnonymizedFrom"] = tmp; } if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom)) { target["ModifiedFrom"] = tmp; } if (type == ResourceType_Patient || type == ResourceType_Study || type == ResourceType_Series) { target["IsStable"] = !index_.IsUnstableResource(internalId); if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate)) { target["LastUpdate"] = tmp; } } found_ = true; } } }; Operations operations(*this); operations.Apply(*this, &target, publicId, level); return operations.HasFound(); } void ServerIndex::GetAllMetadata(std::map<MetadataType, std::string>& target, const std::string& publicId, ResourceType level) { class Operations : public ReadOnlyOperationsT3<std::map<MetadataType, std::string>*, std::string, ResourceType> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { ResourceType type; int64_t id; if (!transaction.LookupResource(id, type, tuple.get<1>()) || tuple.get<2>() != type) { throw OrthancException(ErrorCode_UnknownResource); } else { transaction.GetAllMetadata(*tuple.get<0>(), id); } } }; Operations operations; operations.Apply(*this, &target, publicId, level); } bool ServerIndex::LookupAttachment(FileInfo& attachment, const std::string& instancePublicId, FileContentType contentType) { class Operations : public ReadOnlyOperationsT3<FileInfo*, std::string, FileContentType> { private: bool found_; public: Operations() : found_(false) { } bool HasFound() const { return found_; } virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { int64_t internalId; ResourceType type; if (!transaction.LookupResource(internalId, type, tuple.get<1>())) { throw OrthancException(ErrorCode_UnknownResource); } else if (transaction.LookupAttachment(*tuple.get<0>(), internalId, tuple.get<2>())) { assert(tuple.get<0>()->GetContentType() == tuple.get<2>()); found_ = true; } else { found_ = false; } } }; Operations operations; operations.Apply(*this, &attachment, instancePublicId, contentType); return operations.HasFound(); } void ServerIndex::GetAllUuids(std::list<std::string>& target, ResourceType resourceType) { class Operations : public ReadOnlyOperationsT2<std::list<std::string>*, ResourceType> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { // TODO - CANDIDATE FOR "TransactionType_SingleStatement" transaction.GetAllPublicIds(*tuple.get<0>(), tuple.get<1>()); } }; Operations operations; operations.Apply(*this, &target, resourceType); } void ServerIndex::GetAllUuids(std::list<std::string>& target, ResourceType resourceType, size_t since, size_t limit) { if (limit == 0) { target.clear(); } else { class Operations : public ReadOnlyOperationsT4<std::list<std::string>*, ResourceType, size_t, size_t> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { // TODO - CANDIDATE FOR "TransactionType_SingleStatement" transaction.GetAllPublicIds(*tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); } }; Operations operations; operations.Apply(*this, &target, resourceType, since, limit); } } void ServerIndex::GetGlobalStatistics(/* out */ uint64_t& diskSize, /* out */ uint64_t& uncompressedSize, /* out */ uint64_t& countPatients, /* out */ uint64_t& countStudies, /* out */ uint64_t& countSeries, /* out */ uint64_t& countInstances) { class Operations : public ReadOnlyOperationsT6<uint64_t*, uint64_t*, uint64_t*, uint64_t*, uint64_t*, uint64_t*> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { *tuple.get<0>() = transaction.GetTotalCompressedSize(); *tuple.get<1>() = transaction.GetTotalUncompressedSize(); *tuple.get<2>() = transaction.GetResourceCount(ResourceType_Patient); *tuple.get<3>() = transaction.GetResourceCount(ResourceType_Study); *tuple.get<4>() = transaction.GetResourceCount(ResourceType_Series); *tuple.get<5>() = transaction.GetResourceCount(ResourceType_Instance); } }; Operations operations; operations.Apply(*this, &diskSize, &uncompressedSize, &countPatients, &countStudies, &countSeries, &countInstances); } void ServerIndex::GetChanges(Json::Value& target, int64_t since, unsigned int maxResults) { class Operations : public ReadOnlyOperationsT3<Json::Value*, int64_t, unsigned int> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { // NB: In Orthanc <= 1.3.2, a transaction was missing, as // "GetLastChange()" involves calls to "GetPublicId()" std::list<ServerIndexChange> changes; bool done; bool hasLast = false; int64_t last = 0; transaction.GetChanges(changes, done, tuple.get<1>(), tuple.get<2>()); if (changes.empty()) { last = transaction.GetLastChangeIndex(); hasLast = true; } FormatLog(*tuple.get<0>(), changes, "Changes", done, tuple.get<1>(), hasLast, last); } }; Operations operations; operations.Apply(*this, &target, since, maxResults); } void ServerIndex::GetLastChange(Json::Value& target) { class Operations : public ReadOnlyOperationsT1<Json::Value*> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { // NB: In Orthanc <= 1.3.2, a transaction was missing, as // "GetLastChange()" involves calls to "GetPublicId()" std::list<ServerIndexChange> changes; bool hasLast = false; int64_t last = 0; transaction.GetLastChange(changes); if (changes.empty()) { last = transaction.GetLastChangeIndex(); hasLast = true; } FormatLog(*tuple.get<0>(), changes, "Changes", true, 0, hasLast, last); } }; Operations operations; operations.Apply(*this, &target); } void ServerIndex::GetExportedResources(Json::Value& target, int64_t since, unsigned int maxResults) { class Operations : public ReadOnlyOperationsT3<Json::Value*, int64_t, unsigned int> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { // TODO - CANDIDATE FOR "TransactionType_SingleStatement" std::list<ExportedResource> exported; bool done; transaction.GetExportedResources(exported, done, tuple.get<1>(), tuple.get<2>()); FormatLog(*tuple.get<0>(), exported, "Exports", done, tuple.get<1>(), false, -1); } }; Operations operations; operations.Apply(*this, &target, since, maxResults); } void ServerIndex::GetLastExportedResource(Json::Value& target) { class Operations : public ReadOnlyOperationsT1<Json::Value*> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { // TODO - CANDIDATE FOR "TransactionType_SingleStatement" std::list<ExportedResource> exported; transaction.GetLastExportedResource(exported); FormatLog(*tuple.get<0>(), exported, "Exports", true, 0, false, -1); } }; Operations operations; operations.Apply(*this, &target); } }