# HG changeset patch # User Sebastien Jodogne # Date 1615310699 -3600 # Node ID 1d96fe7e054e5dc672a366b57bbce189964a82c4 # Parent f0bdd99f3d815bf3aa787c2627f0ecab583e2d1d taking StatelessDatabaseOperations out of ServerIndex diff -r f0bdd99f3d81 -r 1d96fe7e054e OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Tue Mar 09 16:40:38 2021 +0100 +++ b/OrthancServer/CMakeLists.txt Tue Mar 09 18:24:59 2021 +0100 @@ -98,6 +98,7 @@ ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/SetOfResources.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/ResourcesContent.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/SQLiteDatabaseWrapper.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/StatelessDatabaseOperations.cpp ${CMAKE_SOURCE_DIR}/Sources/DicomInstanceOrigin.cpp ${CMAKE_SOURCE_DIR}/Sources/DicomInstanceToStore.cpp ${CMAKE_SOURCE_DIR}/Sources/EmbeddedResourceHttpHandler.cpp diff -r f0bdd99f3d81 -r 1d96fe7e054e OrthancServer/Sources/Database/IDatabaseWrapper.h --- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Mar 09 16:40:38 2021 +0100 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Mar 09 18:24:59 2021 +0100 @@ -64,6 +64,9 @@ virtual void Rollback() = 0; + // The "fileSizeDelta" is used for older database plugins that + // have no fast way to compute the size of all the stored + // attachments (cf. "fastGetTotalSize_") virtual void Commit(int64_t fileSizeDelta) = 0; }; diff -r f0bdd99f3d81 -r 1d96fe7e054e OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Mar 09 18:24:59 2021 +0100 @@ -0,0 +1,3165 @@ +/** + * 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 . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "StatelessDatabaseOperations.h" + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" +#include "../../../OrthancFramework/Sources/Logging.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../OrthancConfiguration.h" +#include "../Search/DatabaseLookup.h" +#include "../ServerIndexChange.h" +#include "../ServerToolbox.h" +#include "ResourcesContent.h" + +#include +#include +#include + + +namespace Orthanc +{ + namespace + { + /** + * Some handy templates to reduce the verbosity in the definitions + * of the internal classes. + **/ + + template + class TupleOperationsWrapper : public StatelessDatabaseOperations::IReadOnlyOperations + { + protected: + Operations& operations_; + const Tuple& tuple_; + + public: + TupleOperationsWrapper(Operations& operations, + const Tuple& tuple) : + operations_(operations), + tuple_(tuple) + { + } + + virtual void Apply(StatelessDatabaseOperations::ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE + { + operations_.ApplyTuple(transaction, tuple_); + } + }; + + + template + class ReadOnlyOperationsT1 : public boost::noncopyable + { + public: + typedef typename boost::tuple Tuple; + + virtual ~ReadOnlyOperationsT1() + { + } + + virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction, + const Tuple& tuple) = 0; + + void Apply(StatelessDatabaseOperations& index, + T1 t1) + { + const Tuple tuple(t1); + TupleOperationsWrapper wrapper(*this, tuple); + index.Apply(wrapper); + } + }; + + + template + class ReadOnlyOperationsT2 : public boost::noncopyable + { + public: + typedef typename boost::tuple Tuple; + + virtual ~ReadOnlyOperationsT2() + { + } + + virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction, + const Tuple& tuple) = 0; + + void Apply(StatelessDatabaseOperations& index, + T1 t1, + T2 t2) + { + const Tuple tuple(t1, t2); + TupleOperationsWrapper wrapper(*this, tuple); + index.Apply(wrapper); + } + }; + + + template + class ReadOnlyOperationsT3 : public boost::noncopyable + { + public: + typedef typename boost::tuple Tuple; + + virtual ~ReadOnlyOperationsT3() + { + } + + virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction, + const Tuple& tuple) = 0; + + void Apply(StatelessDatabaseOperations& index, + T1 t1, + T2 t2, + T3 t3) + { + const Tuple tuple(t1, t2, t3); + TupleOperationsWrapper wrapper(*this, tuple); + index.Apply(wrapper); + } + }; + + + template + class ReadOnlyOperationsT4 : public boost::noncopyable + { + public: + typedef typename boost::tuple Tuple; + + virtual ~ReadOnlyOperationsT4() + { + } + + virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction, + const Tuple& tuple) = 0; + + void Apply(StatelessDatabaseOperations& index, + T1 t1, + T2 t2, + T3 t3, + T4 t4) + { + const Tuple tuple(t1, t2, t3, t4); + TupleOperationsWrapper wrapper(*this, tuple); + index.Apply(wrapper); + } + }; + + + template + class ReadOnlyOperationsT5 : public boost::noncopyable + { + public: + typedef typename boost::tuple Tuple; + + virtual ~ReadOnlyOperationsT5() + { + } + + virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction, + const Tuple& tuple) = 0; + + void Apply(StatelessDatabaseOperations& index, + T1 t1, + T2 t2, + T3 t3, + T4 t4, + T5 t5) + { + const Tuple tuple(t1, t2, t3, t4, t5); + TupleOperationsWrapper wrapper(*this, tuple); + index.Apply(wrapper); + } + }; + + + template + class ReadOnlyOperationsT6 : public boost::noncopyable + { + public: + typedef typename boost::tuple Tuple; + + virtual ~ReadOnlyOperationsT6() + { + } + + virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction, + const Tuple& tuple) = 0; + + void Apply(StatelessDatabaseOperations& index, + T1 t1, + T2 t2, + T3 t3, + T4 t4, + T5 t5, + T6 t6) + { + const Tuple tuple(t1, t2, t3, t4, t5, t6); + TupleOperationsWrapper wrapper(*this, tuple); + index.Apply(wrapper); + } + }; + } + + + template + static void FormatLog(Json::Value& target, + const std::list& log, + const std::string& name, + bool done, + int64_t since, + bool hasLast, + int64_t last) + { + Json::Value items = Json::arrayValue; + for (typename std::list::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(last); + } + + + static void CopyListToVector(std::vector& target, + const std::list& source) + { + target.resize(source.size()); + + size_t pos = 0; + + for (std::list::const_iterator + it = source.begin(); it != source.end(); ++it) + { + target[pos] = *it; + pos ++; + } + } + + + class StatelessDatabaseOperations::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 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 tags; + DicomMap::GetMainDicomTags(tags, level); + + for (std::set::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(); + } + } + }; + + + void StatelessDatabaseOperations::ReadWriteTransaction::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); + } + + GetTransactionContext().SignalChange(change); + } + + + SeriesStatus StatelessDatabaseOperations::ReadOnlyTransaction::GetSeriesStatus(int64_t id, + int64_t expectedNumberOfInstances) + { + std::list values; + db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries); + + std::set instances; + + for (std::list::const_iterator + it = values.begin(); it != values.end(); ++it) + { + int64_t index; + + try + { + index = boost::lexical_cast(*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(instances.size()) == expectedNumberOfInstances) + { + return SeriesStatus_Complete; + } + else + { + return SeriesStatus_Missing; + } + } + + + void StatelessDatabaseOperations::NormalizeLookup(std::vector& 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)); + } + } + } + + + class StatelessDatabaseOperations::Transaction : public boost::noncopyable + { + private: + IDatabaseWrapper& db_; + std::unique_ptr transaction_; + std::unique_ptr context_; + bool isCommitted_; + + public: + Transaction(IDatabaseWrapper& db, + ITransactionContextFactory& factory, + TransactionType type) : + db_(db), + isCommitted_(false) + { + context_.reset(factory.Create()); + if (context_.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + transaction_.reset(db_.StartTransaction(type)); + if (transaction_.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + ~Transaction() + { + if (!isCommitted_) + { + try + { + transaction_->Rollback(); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Cannot rollback transaction: " << e.What(); + } + } + } + + void Commit() + { + if (isCommitted_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + int64_t delta = context_->GetCompressedSizeDelta(); + + transaction_->Commit(delta); + context_->Commit(); + isCommitted_ = true; + } + } + + ITransactionContext& GetContext() const + { + assert(context_.get() != NULL); + return *context_; + } + }; + + + void StatelessDatabaseOperations::ApplyInternal(IReadOnlyOperations* readOperations, + IReadWriteOperations* writeOperations) + { + if ((readOperations == NULL && writeOperations == NULL) || + (readOperations != NULL && writeOperations != NULL)) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (factory_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "No transaction context was provided"); + } + + unsigned int count = 0; + + for (;;) + { + try + { + boost::mutex::scoped_lock lock(databaseMutex_); // TODO - REMOVE + + if (readOperations != NULL) + { + /** + * IMPORTANT: In Orthanc <= 1.9.1, there was no transaction + * in this case. This was OK because of the presence of the + * global mutex protecting the database. + **/ + + Transaction transaction(db_, *factory_, TransactionType_ReadOnly); // TODO - Only if not "TransactionType_Implicit" + { + ReadOnlyTransaction t(db_, transaction.GetContext()); + readOperations->Apply(t); + } + transaction.Commit(); + } + else + { + assert(writeOperations != NULL); + + Transaction transaction(db_, *factory_, TransactionType_ReadWrite); + { + ReadWriteTransaction t(db_, transaction.GetContext()); + writeOperations->Apply(t); + } + transaction.Commit(); + } + + 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; + } + } + } + } + + + StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : + db_(db), + maxRetries_(10), + mainDicomTagsRegistry_(new MainDicomTagsRegistry) + { + } + + + void StatelessDatabaseOperations::SetTransactionContextFactory(ITransactionContextFactory* factory) + { + if (factory == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (factory_.get() != NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + factory_.reset(factory); + } + } + + + void StatelessDatabaseOperations::Apply(IReadOnlyOperations& operations) + { + ApplyInternal(&operations, NULL); + } + + + void StatelessDatabaseOperations::Apply(IReadWriteOperations& operations) + { + ApplyInternal(NULL, &operations); + } + + + bool StatelessDatabaseOperations::ExpandResource(Json::Value& target, + const std::string& publicId, + ResourceType level) + { + class Operations : public ReadOnlyOperationsT4 + { + private: + static void MainDicomTagsToJson(ReadOnlyTransaction& transaction, + Json::Value& target, + int64_t resourceId, + ResourceType resourceType) + { + DicomMap tags; + transaction.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); + } + } + + + static bool LookupStringMetadata(std::string& result, + const std::map& metadata, + MetadataType type) + { + std::map::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& metadata, + MetadataType type) + { + std::string s; + if (!LookupStringMetadata(s, metadata, type)) + { + return false; + } + + try + { + result = boost::lexical_cast(s); + return true; + } + catch (boost::bad_lexical_cast&) + { + return false; + } + } + + + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // Lookup for the requested resource + int64_t internalId; // unused + ResourceType type; + std::string parent; + if (!transaction.LookupResourceAndParent(internalId, type, parent, tuple.get<2>()) || + type != tuple.get<3>()) + { + tuple.get<0>() = false; + } + else + { + Json::Value& target = tuple.get<1>(); + 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 children; + transaction.GetChildrenPublicId(children, internalId); + + if (type != ResourceType_Instance) + { + Json::Value c = Json::arrayValue; + + for (std::list::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 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(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(attachment.GetUncompressedSize()); + target["FileUuid"] = attachment.GetUuid(); + + int64_t i; + if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries)) + { + target["IndexInSeries"] = static_cast(i); + } + else + { + target["IndexInSeries"] = Json::nullValue; + } + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + // Record the remaining information + target["ID"] = tuple.get<2>(); + MainDicomTagsToJson(transaction, 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"] = !transaction.GetTransactionContext().IsUnstableResource(internalId); + + if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate)) + { + target["LastUpdate"] = tmp; + } + } + + tuple.get<0>() = true; + } + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, target, publicId, level); + return found; + } + + + void StatelessDatabaseOperations::GetAllMetadata(std::map& target, + const std::string& publicId, + ResourceType level) + { + class Operations : public ReadOnlyOperationsT3&, const 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 StatelessDatabaseOperations::LookupAttachment(FileInfo& attachment, + const std::string& instancePublicId, + FileContentType contentType) + { + class Operations : public ReadOnlyOperationsT4 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + int64_t internalId; + ResourceType type; + if (!transaction.LookupResource(internalId, type, tuple.get<2>())) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else if (transaction.LookupAttachment(tuple.get<1>(), internalId, tuple.get<3>())) + { + assert(tuple.get<1>().GetContentType() == tuple.get<3>()); + tuple.get<0>() = true; + } + else + { + tuple.get<0>() = false; + } + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, attachment, instancePublicId, contentType); + return found; + } + + + void StatelessDatabaseOperations::GetAllUuids(std::list& target, + ResourceType resourceType) + { + class Operations : public ReadOnlyOperationsT2&, ResourceType> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // TODO - CANDIDATE FOR "TransactionType_Implicit" + transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>()); + } + }; + + Operations operations; + operations.Apply(*this, target, resourceType); + } + + + void StatelessDatabaseOperations::GetAllUuids(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) + { + if (limit == 0) + { + target.clear(); + } + else + { + class Operations : public ReadOnlyOperationsT4&, ResourceType, size_t, size_t> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // TODO - CANDIDATE FOR "TransactionType_Implicit" + transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); + } + }; + + Operations operations; + operations.Apply(*this, target, resourceType, since, limit); + } + } + + + void StatelessDatabaseOperations::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 + { + 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 StatelessDatabaseOperations::GetChanges(Json::Value& target, + int64_t since, + unsigned int maxResults) + { + class Operations : public ReadOnlyOperationsT3 + { + 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 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 StatelessDatabaseOperations::GetLastChange(Json::Value& target) + { + class Operations : public ReadOnlyOperationsT1 + { + 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 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 StatelessDatabaseOperations::GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults) + { + class Operations : public ReadOnlyOperationsT3 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // TODO - CANDIDATE FOR "TransactionType_Implicit" + + std::list 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 StatelessDatabaseOperations::GetLastExportedResource(Json::Value& target) + { + class Operations : public ReadOnlyOperationsT1 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // TODO - CANDIDATE FOR "TransactionType_Implicit" + + std::list exported; + transaction.GetLastExportedResource(exported); + FormatLog(tuple.get<0>(), exported, "Exports", true, 0, false, -1); + } + }; + + Operations operations; + operations.Apply(*this, target); + } + + + bool StatelessDatabaseOperations::IsProtectedPatient(const std::string& publicId) + { + class Operations : public ReadOnlyOperationsT2 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // Lookup for the requested resource + int64_t id; + ResourceType type; + if (!transaction.LookupResource(id, type, tuple.get<1>()) || + type != ResourceType_Patient) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + tuple.get<0>() = transaction.IsProtectedPatient(id); + } + } + }; + + bool isProtected; + Operations operations; + operations.Apply(*this, isProtected, publicId); + return isProtected; + } + + + void StatelessDatabaseOperations::GetChildren(std::list& result, + const std::string& publicId) + { + class Operations : public ReadOnlyOperationsT2&, const std::string&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + ResourceType type; + int64_t resource; + if (!transaction.LookupResource(resource, type, tuple.get<1>())) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else if (type == ResourceType_Instance) + { + // An instance cannot have a child + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + std::list tmp; + transaction.GetChildrenInternalId(tmp, resource); + + tuple.get<0>().clear(); + + for (std::list::const_iterator + it = tmp.begin(); it != tmp.end(); ++it) + { + tuple.get<0>().push_back(transaction.GetPublicId(*it)); + } + } + } + }; + + Operations operations; + operations.Apply(*this, result, publicId); + } + + + void StatelessDatabaseOperations::GetChildInstances(std::list& result, + const std::string& publicId) + { + class Operations : public ReadOnlyOperationsT2&, const std::string&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + tuple.get<0>().clear(); + + ResourceType type; + int64_t top; + if (!transaction.LookupResource(top, type, tuple.get<1>())) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else if (type == ResourceType_Instance) + { + // The resource is already an instance: Do not go down the hierarchy + tuple.get<0>().push_back(tuple.get<1>()); + } + else + { + std::stack toExplore; + toExplore.push(top); + + std::list tmp; + while (!toExplore.empty()) + { + // Get the internal ID of the current resource + int64_t resource = toExplore.top(); + toExplore.pop(); + + // TODO - This could be optimized by seeing how many + // levels "type == transaction.GetResourceType(top)" is + // above the "instances level" + if (transaction.GetResourceType(resource) == ResourceType_Instance) + { + tuple.get<0>().push_back(transaction.GetPublicId(resource)); + } + else + { + // Tag all the children of this resource as to be explored + transaction.GetChildrenInternalId(tmp, resource); + for (std::list::const_iterator + it = tmp.begin(); it != tmp.end(); ++it) + { + toExplore.push(*it); + } + } + } + } + } + }; + + Operations operations; + operations.Apply(*this, result, publicId); + } + + + bool StatelessDatabaseOperations::LookupMetadata(std::string& target, + const std::string& publicId, + ResourceType expectedType, + MetadataType type) + { + class Operations : public ReadOnlyOperationsT5 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + ResourceType rtype; + int64_t id; + if (!transaction.LookupResource(id, rtype, tuple.get<2>()) || + rtype != tuple.get<3>()) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + tuple.get<0>() = transaction.LookupMetadata(tuple.get<1>(), id, tuple.get<4>()); + } + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, target, publicId, expectedType, type); + return found; + } + + + void StatelessDatabaseOperations::ListAvailableAttachments(std::set& target, + const std::string& publicId, + ResourceType expectedType) + { + class Operations : public ReadOnlyOperationsT3&, const 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.ListAvailableAttachments(tuple.get<0>(), id); + } + } + }; + + Operations operations; + operations.Apply(*this, target, publicId, expectedType); + } + + + bool StatelessDatabaseOperations::LookupParent(std::string& target, + const std::string& publicId) + { + class Operations : public ReadOnlyOperationsT3 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + ResourceType type; + int64_t id; + if (!transaction.LookupResource(id, type, tuple.get<2>())) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + int64_t parentId; + if (transaction.LookupParent(parentId, id)) + { + tuple.get<1>() = transaction.GetPublicId(parentId); + tuple.get<0>() = true; + } + else + { + tuple.get<0>() = false; + } + } + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, target, publicId); + return found; + } + + + void StatelessDatabaseOperations::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) + { + class Operations : public IReadOnlyOperations + { + private: + ResourceType& type_; + uint64_t& diskSize_; + uint64_t& uncompressedSize_; + unsigned int& countStudies_; + unsigned int& countSeries_; + unsigned int& countInstances_; + uint64_t& dicomDiskSize_; + uint64_t& dicomUncompressedSize_; + const std::string& publicId_; + + public: + explicit Operations(ResourceType& type, + uint64_t& diskSize, + uint64_t& uncompressedSize, + unsigned int& countStudies, + unsigned int& countSeries, + unsigned int& countInstances, + uint64_t& dicomDiskSize, + uint64_t& dicomUncompressedSize, + const std::string& publicId) : + type_(type), + diskSize_(diskSize), + uncompressedSize_(uncompressedSize), + countStudies_(countStudies), + countSeries_(countSeries), + countInstances_(countInstances), + dicomDiskSize_(dicomDiskSize), + dicomUncompressedSize_(dicomUncompressedSize), + publicId_(publicId) + { + } + + virtual void Apply(ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE + { + int64_t top; + if (!transaction.LookupResource(top, type_, publicId_)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + countInstances_ = 0; + countSeries_ = 0; + countStudies_ = 0; + diskSize_ = 0; + uncompressedSize_ = 0; + dicomDiskSize_ = 0; + dicomUncompressedSize_ = 0; + + std::stack toExplore; + toExplore.push(top); + + while (!toExplore.empty()) + { + // Get the internal ID of the current resource + int64_t resource = toExplore.top(); + toExplore.pop(); + + ResourceType thisType = transaction.GetResourceType(resource); + + std::set f; + transaction.ListAvailableAttachments(f, resource); + + for (std::set::const_iterator + it = f.begin(); it != f.end(); ++it) + { + FileInfo attachment; + if (transaction.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 tmp; + transaction.GetChildrenInternalId(tmp, resource); + for (std::list::const_iterator + it = tmp.begin(); it != tmp.end(); ++it) + { + toExplore.push(*it); + } + } + } + + if (countStudies_ == 0) + { + countStudies_ = 1; + } + + if (countSeries_ == 0) + { + countSeries_ = 1; + } + } + } + }; + + Operations operations(type, diskSize, uncompressedSize, countStudies, countSeries, + countInstances, dicomDiskSize, dicomUncompressedSize, publicId); + Apply(operations); + } + + + void StatelessDatabaseOperations::LookupIdentifierExact(std::vector& 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 query; + query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + + + class Operations : public IReadOnlyOperations + { + private: + std::vector& result_; + const std::vector& query_; + ResourceType level_; + + public: + Operations(std::vector& result, + const std::vector& query, + ResourceType level) : + result_(result), + query_(query), + level_(level) + { + } + + virtual void Apply(ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE + { + // TODO - CANDIDATE FOR "TransactionType_Implicit" + std::list tmp; + transaction.ApplyLookupResources(tmp, NULL, query_, level_, 0); + CopyListToVector(result_, tmp); + } + }; + + Operations operations(result, query, level); + Apply(operations); + } + + + bool StatelessDatabaseOperations::LookupGlobalProperty(std::string& value, + GlobalProperty property) + { + class Operations : public ReadOnlyOperationsT3 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // TODO - CANDIDATE FOR "TransactionType_Implicit" + tuple.get<0>() = transaction.LookupGlobalProperty(tuple.get<1>(), tuple.get<2>()); + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, value, property); + return found; + } + + + std::string StatelessDatabaseOperations::GetGlobalProperty(GlobalProperty property, + const std::string& defaultValue) + { + std::string s; + if (LookupGlobalProperty(s, property)) + { + return s; + } + else + { + return defaultValue; + } + } + + + bool StatelessDatabaseOperations::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); + } + + + class Operations : public ReadOnlyOperationsT5 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // Lookup for the requested resource + int64_t id; + ResourceType type; + if (!transaction.LookupResource(id, type, tuple.get<2>()) || + type != tuple.get<3>()) + { + tuple.get<0>() = false; + } + else if (type == ResourceType_Study) + { + DicomMap tmp; + transaction.GetMainDicomTags(tmp, id); + + switch (tuple.get<4>()) + { + case ResourceType_Patient: + tmp.ExtractPatientInformation(tuple.get<1>()); + tuple.get<0>() = true; + break; + + case ResourceType_Study: + tmp.ExtractStudyInformation(tuple.get<1>()); + tuple.get<0>() = true; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + transaction.GetMainDicomTags(tuple.get<1>(), id); + tuple.get<0>() = true; + } + } + }; + + result.Clear(); + + bool found; + Operations operations; + operations.Apply(*this, found, result, publicId, expectedType, levelOfInterest); + return found; + } + + + bool StatelessDatabaseOperations::GetAllMainDicomTags(DicomMap& result, + const std::string& instancePublicId) + { + class Operations : public ReadOnlyOperationsT3 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // Lookup for the requested resource + int64_t instance; + ResourceType type; + if (!transaction.LookupResource(instance, type, tuple.get<2>()) || + type != ResourceType_Instance) + { + tuple.get<0>() = false; + } + else + { + DicomMap tmp; + + transaction.GetMainDicomTags(tmp, instance); + tuple.get<1>().Merge(tmp); + + int64_t series; + if (!transaction.LookupParent(series, instance)) + { + throw OrthancException(ErrorCode_InternalError); + } + + tmp.Clear(); + transaction.GetMainDicomTags(tmp, series); + tuple.get<1>().Merge(tmp); + + int64_t study; + if (!transaction.LookupParent(study, series)) + { + throw OrthancException(ErrorCode_InternalError); + } + + tmp.Clear(); + transaction.GetMainDicomTags(tmp, study); + tuple.get<1>().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 (!transaction.LookupParent(patient, study)) + { + throw OrthancException(ErrorCode_InternalError); + } + + tmp.Clear(); + transaction.GetMainDicomTags(tmp, study); + + std::set patientTags; + tmp.GetTags(patientTags); + + for (std::set::const_iterator + it = patientTags.begin(); it != patientTags.end(); ++it) + { + assert(tuple.get<1>().HasTag(*it)); + } + } +#endif + + tuple.get<0>() = true; + } + } + }; + + result.Clear(); + + bool found; + Operations operations; + operations.Apply(*this, found, result, instancePublicId); + return found; + } + + + bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type, + const std::string& publicId) + { + class Operations : public ReadOnlyOperationsT3 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // TODO - CANDIDATE FOR "TransactionType_Implicit" + int64_t id; + tuple.get<0>() = transaction.LookupResource(id, tuple.get<1>(), tuple.get<2>()); + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, type, publicId); + return found; + } + + + bool StatelessDatabaseOperations::LookupParent(std::string& target, + const std::string& publicId, + ResourceType parentType) + { + class Operations : public ReadOnlyOperationsT4 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + ResourceType type; + int64_t id; + if (!transaction.LookupResource(id, type, tuple.get<2>())) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + while (type != tuple.get<3>()) + { + int64_t parentId; + + if (type == ResourceType_Patient || // Cannot further go up in hierarchy + !transaction.LookupParent(parentId, id)) + { + tuple.get<0>() = false; + return; + } + + id = parentId; + type = GetParentResourceType(type); + } + + tuple.get<0>() = true; + tuple.get<1>() = transaction.GetPublicId(id); + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, target, publicId, parentType); + return found; + } + + + void StatelessDatabaseOperations::ApplyLookupResources(std::vector& resourcesId, + std::vector* instancesId, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t limit) + { + class Operations : public ReadOnlyOperationsT4&, ResourceType, size_t> + { + private: + std::list resourcesList_; + std::list instancesList_; + + public: + const std::list& GetResourcesList() const + { + return resourcesList_; + } + + const std::list& GetInstancesList() const + { + return instancesList_; + } + + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + // TODO - CANDIDATE FOR "TransactionType_Implicit" + if (tuple.get<0>()) + { + transaction.ApplyLookupResources(resourcesList_, &instancesList_, tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); + } + else + { + transaction.ApplyLookupResources(resourcesList_, NULL, tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); + } + } + }; + + + std::vector normalized; + NormalizeLookup(normalized, lookup, queryLevel); + + Operations operations; + operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, limit); + + CopyListToVector(resourcesId, operations.GetResourcesList()); + + if (instancesId != NULL) + { + CopyListToVector(*instancesId, operations.GetInstancesList()); + } + } + + + bool StatelessDatabaseOperations::DeleteResource(Json::Value& target, + const std::string& uuid, + ResourceType expectedType) + { + class Operations : public IReadWriteOperations + { + private: + bool found_; + Json::Value& target_; + const std::string& uuid_; + ResourceType expectedType_; + + public: + Operations(Json::Value& target, + const std::string& uuid, + ResourceType expectedType) : + found_(false), + target_(target), + uuid_(uuid), + expectedType_(expectedType) + { + } + + bool IsFound() const + { + return found_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + int64_t id; + ResourceType type; + if (!transaction.LookupResource(id, type, uuid_) || + expectedType_ != type) + { + found_ = false; + } + else + { + found_ = true; + transaction.DeleteResource(id); + + std::string remainingPublicId; + ResourceType remainingLevel; + if (transaction.GetTransactionContext().LookupRemainingLevel(remainingPublicId, remainingLevel)) + { + target_["RemainingAncestor"] = Json::Value(Json::objectValue); + target_["RemainingAncestor"]["Path"] = GetBasePath(remainingLevel, remainingPublicId); + target_["RemainingAncestor"]["Type"] = EnumerationToString(remainingLevel); + target_["RemainingAncestor"]["ID"] = remainingPublicId; + } + else + { + target_["RemainingAncestor"] = Json::nullValue; + } + } + } + }; + + Operations operations(target, uuid, expectedType); + Apply(operations); + return operations.IsFound(); + } + + + void StatelessDatabaseOperations::LogExportedResource(const std::string& publicId, + const std::string& remoteModality) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& publicId_; + const std::string& remoteModality_; + + public: + Operations(const std::string& publicId, + const std::string& remoteModality) : + publicId_(publicId), + remoteModality_(remoteModality) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + int64_t id; + ResourceType type; + if (!transaction.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; + transaction.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 = transaction.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); + + transaction.LogExportedResource(resource); + } + }; + + Operations operations(publicId, remoteModality); + Apply(operations); + } + + + void StatelessDatabaseOperations::SetProtectedPatient(const std::string& publicId, + bool isProtected) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& publicId_; + bool isProtected_; + + public: + Operations(const std::string& publicId, + bool isProtected) : + publicId_(publicId), + isProtected_(isProtected) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + // Lookup for the requested resource + int64_t id; + ResourceType type; + if (!transaction.LookupResource(id, type, publicId_) || + type != ResourceType_Patient) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + transaction.SetProtectedPatient(id, isProtected_); + } + } + }; + + Operations operations(publicId, isProtected); + Apply(operations); + + if (isProtected) + { + LOG(INFO) << "Patient " << publicId << " has been protected"; + } + else + { + LOG(INFO) << "Patient " << publicId << " has been unprotected"; + } + } + + + void StatelessDatabaseOperations::SetMetadata(const std::string& publicId, + MetadataType type, + const std::string& value) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& publicId_; + MetadataType type_; + const std::string& value_; + + public: + Operations(const std::string& publicId, + MetadataType type, + const std::string& value) : + publicId_(publicId), + type_(type), + value_(value) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + ResourceType rtype; + int64_t id; + if (!transaction.LookupResource(id, rtype, publicId_)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + transaction.SetMetadata(id, type_, value_); + + if (IsUserMetadata(type_)) + { + transaction.LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId_); + } + } + } + }; + + Operations operations(publicId, type, value); + Apply(operations); + } + + + void StatelessDatabaseOperations::DeleteMetadata(const std::string& publicId, + MetadataType type) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& publicId_; + MetadataType type_; + + public: + Operations(const std::string& publicId, + MetadataType type) : + publicId_(publicId), + type_(type) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + ResourceType rtype; + int64_t id; + if (!transaction.LookupResource(id, rtype, publicId_)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + transaction.DeleteMetadata(id, type_); + + if (IsUserMetadata(type_)) + { + transaction.LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId_); + } + } + } + }; + + Operations operations(publicId, type); + Apply(operations); + } + + + uint64_t StatelessDatabaseOperations::IncrementGlobalSequence(GlobalProperty sequence) + { + class Operations : public IReadWriteOperations + { + private: + uint64_t newValue_; + GlobalProperty sequence_; + + public: + explicit Operations(GlobalProperty sequence) : + newValue_(0), // Dummy initialization + sequence_(sequence) + { + } + + uint64_t GetNewValue() const + { + return newValue_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + std::string oldString; + + if (transaction.LookupGlobalProperty(oldString, sequence_)) + { + uint64_t oldValue; + + try + { + oldValue = boost::lexical_cast(oldString); + } + catch (boost::bad_lexical_cast&) + { + LOG(ERROR) << "Cannot read the global sequence " + << boost::lexical_cast(sequence_) << ", resetting it"; + oldValue = 0; + } + + newValue_ = oldValue + 1; + } + else + { + // Initialize the sequence at "1" + newValue_ = 1; + } + + transaction.SetGlobalProperty(sequence_, boost::lexical_cast(newValue_)); + } + }; + + Operations operations(sequence); + Apply(operations); + assert(operations.GetNewValue() != 0); + return operations.GetNewValue(); + } + + + void StatelessDatabaseOperations::DeleteChanges() + { + class Operations : public IReadWriteOperations + { + public: + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.ClearChanges(); + } + }; + + Operations operations; + Apply(operations); + } + + + void StatelessDatabaseOperations::DeleteExportedResources() + { + class Operations : public IReadWriteOperations + { + public: + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.ClearExportedResources(); + } + }; + + Operations operations; + Apply(operations); + } + + + void StatelessDatabaseOperations::SetGlobalProperty(GlobalProperty property, + const std::string& value) + { + class Operations : public IReadWriteOperations + { + private: + GlobalProperty property_; + const std::string& value_; + + public: + Operations(GlobalProperty property, + const std::string& value) : + property_(property), + value_(value) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.SetGlobalProperty(property_, value_); + } + }; + + Operations operations(property, value); + Apply(operations); + } + + + void StatelessDatabaseOperations::DeleteAttachment(const std::string& publicId, + FileContentType type) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& publicId_; + FileContentType type_; + + public: + Operations(const std::string& publicId, + FileContentType type) : + publicId_(publicId), + type_(type) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + ResourceType rtype; + int64_t id; + if (!transaction.LookupResource(id, rtype, publicId_)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + transaction.DeleteAttachment(id, type_); + + if (IsUserContentType(type_)) + { + transaction.LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId_); + } + } + } + }; + + Operations operations(publicId, type); + Apply(operations); + } + + + void StatelessDatabaseOperations::LogChange(ChangeType changeType, + const std::string& publicId, + ResourceType level) + { + class Operations : public IReadWriteOperations + { + private: + ChangeType changeType_; + const std::string& publicId_; + ResourceType level_; + + public: + Operations(ChangeType changeType, + const std::string& publicId, + ResourceType level) : + changeType_(changeType), + publicId_(publicId), + level_(level) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + int64_t id; + ResourceType type; + if (transaction.LookupResource(id, type, publicId_)) + { + // Make sure that the resource is still existing. Ignore if + // the resource has been deleted, because this function + // might e.g. be called from + // "StatelessDatabaseOperations::UnstableResourcesMonitorThread()" (for + // which a deleted resource not an error case) + if (type == level_) + { + transaction.LogChange(id, changeType_, type, publicId_); + } + else + { + // Consistency check + throw OrthancException(ErrorCode_UnknownResource); + } + } + } + }; + + Operations operations(changeType, publicId, level); + Apply(operations); + } + + + void StatelessDatabaseOperations::ReconstructInstance(const ParsedDicomFile& dicom) + { + class Operations : public IReadWriteOperations + { + private: + DicomMap summary_; + std::unique_ptr hasher_; + bool hasTransferSyntax_; + DicomTransferSyntax transferSyntax_; + + public: + Operations(const ParsedDicomFile& dicom) + { + OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom); + hasher_.reset(new DicomInstanceHasher(summary_)); + hasTransferSyntax_ = dicom.LookupTransferSyntax(transferSyntax_); + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + int64_t patient = -1, study = -1, series = -1, instance = -1; + + ResourceType type1, type2, type3, type4; + if (!transaction.LookupResource(patient, type1, hasher_->HashPatient()) || + !transaction.LookupResource(study, type2, hasher_->HashStudy()) || + !transaction.LookupResource(series, type3, hasher_->HashSeries()) || + !transaction.LookupResource(instance, type4, hasher_->HashInstance()) || + type1 != ResourceType_Patient || + type2 != ResourceType_Study || + type3 != ResourceType_Series || + type4 != ResourceType_Instance || + patient == -1 || + study == -1 || + series == -1 || + instance == -1) + { + throw OrthancException(ErrorCode_InternalError); + } + + transaction.ClearMainDicomTags(patient); + transaction.ClearMainDicomTags(study); + transaction.ClearMainDicomTags(series); + transaction.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_); + transaction.SetResourcesContent(content); + } + + if (hasTransferSyntax_) + { + transaction.SetMetadata(instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_)); + } + + const DicomValue* value; + if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && + !value->IsNull() && + !value->IsBinary()) + { + transaction.SetMetadata(instance, MetadataType_Instance_SopClassUid, value->GetContent()); + } + } + }; + + Operations operations(dicom); + Apply(operations); + } + + + static bool IsRecyclingNeeded(IDatabaseWrapper& db, + uint64_t maximumStorageSize, + unsigned int maximumPatients, + uint64_t addedInstanceSize) + { + if (maximumStorageSize != 0) + { + if (maximumStorageSize < addedInstanceSize) + { + throw OrthancException(ErrorCode_FullStorage, "Cannot store an instance of size " + + boost::lexical_cast(addedInstanceSize) + + " bytes in a storage area limited to " + + boost::lexical_cast(maximumStorageSize)); + } + + if (db.IsDiskSizeAbove(maximumStorageSize - addedInstanceSize)) + { + return true; + } + } + + if (maximumPatients != 0) + { + uint64_t patientCount = db.GetResourceCount(ResourceType_Patient); + if (patientCount > maximumPatients) + { + return true; + } + } + + return false; + } + + + void StatelessDatabaseOperations::ReadWriteTransaction::Recycle(uint64_t maximumStorageSize, + unsigned int maximumPatients, + uint64_t addedInstanceSize, + const std::string& newPatientId) + { + // TODO - Performance: Avoid calls to "IsRecyclingNeeded()" + + if (IsRecyclingNeeded(db_, maximumStorageSize, maximumPatients, addedInstanceSize)) + { + // Check whether other DICOM instances from this patient are + // already stored + int64_t patientToAvoid; + bool hasPatientToAvoid; + + if (newPatientId.empty()) + { + hasPatientToAvoid = false; + } + else + { + ResourceType type; + hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId); + if (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(db_, maximumStorageSize, maximumPatients, addedInstanceSize)) + { + // OK, we're done + return; + } + } + } + } + + + void StatelessDatabaseOperations::StandaloneRecycling(uint64_t maximumStorageSize, + unsigned int maximumPatientCount) + { + class Operations : public IReadWriteOperations + { + private: + uint64_t maximumStorageSize_; + unsigned int maximumPatientCount_; + + public: + Operations(uint64_t maximumStorageSize, + unsigned int maximumPatientCount) : + maximumStorageSize_(maximumStorageSize), + maximumPatientCount_(maximumPatientCount) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.Recycle(maximumStorageSize_, maximumPatientCount_, 0, ""); + } + }; + + Operations operations(maximumStorageSize, maximumPatientCount); + Apply(operations); + } + + + StoreStatus StatelessDatabaseOperations::Store(std::map& instanceMetadata, + const DicomMap& dicomSummary, + const Attachments& attachments, + const MetadataMap& metadata, + const DicomInstanceOrigin& origin, + bool overwrite, + bool hasTransferSyntax, + DicomTransferSyntax transferSyntax, + bool hasPixelDataOffset, + uint64_t pixelDataOffset, + uint64_t maximumStorageSize, + unsigned int maximumPatients) + { + class Operations : public IReadWriteOperations + { + private: + StoreStatus storeStatus_; + std::map& instanceMetadata_; + const DicomMap& dicomSummary_; + const Attachments& attachments_; + const MetadataMap& metadata_; + const DicomInstanceOrigin& origin_; + bool overwrite_; + bool hasTransferSyntax_; + DicomTransferSyntax transferSyntax_; + bool hasPixelDataOffset_; + uint64_t pixelDataOffset_; + uint64_t maximumStorageSize_; + unsigned int maximumPatientCount_; + + // Auto-computed fields + bool hasExpectedInstances_; + int64_t expectedInstances_; + std::string hashPatient_; + std::string hashStudy_; + std::string hashSeries_; + std::string hashInstance_; + + + static void SetInstanceMetadata(ResourcesContent& content, + std::map& instanceMetadata, + int64_t instance, + MetadataType metadata, + const std::string& value) + { + content.AddMetadata(instance, metadata, value); + instanceMetadata[metadata] = value; + } + + + 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(value->GetContent()); + int64_t countTemporalPositions = boost::lexical_cast(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(value->GetContent()); + int64_t numberOfTimeSlices = boost::lexical_cast(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(value->GetContent()); + return (target > 0); + } + } + catch (OrthancException&) + { + } + catch (boost::bad_lexical_cast&) + { + } + + return false; + } + + public: + Operations(std::map& instanceMetadata, + const DicomMap& dicomSummary, + const Attachments& attachments, + const MetadataMap& metadata, + const DicomInstanceOrigin& origin, + bool overwrite, + bool hasTransferSyntax, + DicomTransferSyntax transferSyntax, + bool hasPixelDataOffset, + uint64_t pixelDataOffset, + uint64_t maximumStorageSize, + unsigned int maximumPatientCount) : + storeStatus_(StoreStatus_Failure), + instanceMetadata_(instanceMetadata), + dicomSummary_(dicomSummary), + attachments_(attachments), + metadata_(metadata), + origin_(origin), + overwrite_(overwrite), + hasTransferSyntax_(hasTransferSyntax), + transferSyntax_(transferSyntax), + hasPixelDataOffset_(hasPixelDataOffset), + pixelDataOffset_(pixelDataOffset), + maximumStorageSize_(maximumStorageSize), + maximumPatientCount_(maximumPatientCount) + { + hasExpectedInstances_ = ComputeExpectedNumberOfInstances(expectedInstances_, dicomSummary); + + instanceMetadata_.clear(); + + DicomInstanceHasher hasher(dicomSummary); + hashPatient_ = hasher.HashPatient(); + hashStudy_ = hasher.HashStudy(); + hashSeries_ = hasher.HashSeries(); + hashInstance_ = hasher.HashInstance(); + } + + StoreStatus GetStoreStatus() const + { + return storeStatus_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + try + { + IDatabaseWrapper::CreateInstanceResult status; + int64_t instanceId; + + // Check whether this instance is already stored + if (!transaction.CreateInstance(status, instanceId, hashPatient_, + hashStudy_, hashSeries_, hashInstance_)) + { + // The instance already exists + + if (overwrite_) + { + // Overwrite the old instance + LOG(INFO) << "Overwriting instance: " << hashInstance_; + transaction.DeleteResource(instanceId); + + // Re-create the instance, now that the old one is removed + if (!transaction.CreateInstance(status, instanceId, hashPatient_, + hashStudy_, hashSeries_, hashInstance_)) + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + // Do nothing if the instance already exists and overwriting is disabled + transaction.GetAllMetadata(instanceMetadata_, instanceId); + storeStatus_ = StoreStatus_AlreadyStored; + return; + } + } + + + // 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 "transaction.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. + transaction.LogChange(instanceId, ChangeType_NewInstance, ResourceType_Instance, hashInstance_); + + if (status.isNewSeries_) + { + transaction.LogChange(status.seriesId_, ChangeType_NewSeries, ResourceType_Series, hashSeries_); + } + + if (status.isNewStudy_) + { + transaction.LogChange(status.studyId_, ChangeType_NewStudy, ResourceType_Study, hashStudy_); + } + + if (status.isNewPatient_) + { + transaction.LogChange(status.patientId_, ChangeType_NewPatient, ResourceType_Patient, hashPatient_); + } + + + // 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(); + } + + transaction.Recycle(maximumStorageSize_, maximumPatientCount_, + 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) + { + transaction.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(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(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())); + } + } + + + transaction.SetResourcesContent(content); + } + + + // Check whether the series of this new instance is now completed + int64_t expectedNumberOfInstances; + if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary_)) + { + SeriesStatus seriesStatus = transaction.GetSeriesStatus(status.seriesId_, expectedNumberOfInstances); + if (seriesStatus == SeriesStatus_Complete) + { + transaction.LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries_); + } + } + + transaction.LogChange(status.seriesId_, ChangeType_NewChildInstance, ResourceType_Series, hashSeries_); + transaction.LogChange(status.studyId_, ChangeType_NewChildInstance, ResourceType_Study, hashStudy_); + transaction.LogChange(status.patientId_, ChangeType_NewChildInstance, ResourceType_Patient, hashPatient_); + + // Mark the parent resources of this instance as unstable + transaction.GetTransactionContext().MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries_); + transaction.GetTransactionContext().MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy_); + transaction.GetTransactionContext().MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient_); + transaction.GetTransactionContext().SignalAttachmentsAdded(instanceSize); + + storeStatus_ = StoreStatus_Success; + } + catch (OrthancException& e) + { + LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; + storeStatus_ = StoreStatus_Failure; + } + } + }; + + + Operations operations(instanceMetadata, dicomSummary, attachments, metadata, origin, + overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset, + pixelDataOffset, maximumStorageSize, maximumPatients); + Apply(operations); + return operations.GetStoreStatus(); + } + + + StoreStatus StatelessDatabaseOperations::AddAttachment(const FileInfo& attachment, + const std::string& publicId, + uint64_t maximumStorageSize, + unsigned int maximumPatients) + { + class Operations : public IReadWriteOperations + { + private: + StoreStatus status_; + const FileInfo& attachment_; + const std::string& publicId_; + uint64_t maximumStorageSize_; + unsigned int maximumPatientCount_; + + public: + Operations(const FileInfo& attachment, + const std::string& publicId, + uint64_t maximumStorageSize, + unsigned int maximumPatientCount) : + status_(StoreStatus_Failure), + attachment_(attachment), + publicId_(publicId), + maximumStorageSize_(maximumStorageSize), + maximumPatientCount_(maximumPatientCount) + { + } + + StoreStatus GetStatus() const + { + return status_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + ResourceType resourceType; + int64_t resourceId; + if (!transaction.LookupResource(resourceId, resourceType, publicId_)) + { + status_ = StoreStatus_Failure; // Inexistent resource + } + else + { + // Remove possible previous attachment + transaction.DeleteAttachment(resourceId, attachment_.GetContentType()); + + // Locate the patient of the target resource + int64_t patientId = resourceId; + for (;;) + { + int64_t parent; + if (transaction.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(transaction.GetResourceType(patientId) == ResourceType_Patient); + transaction.Recycle(maximumStorageSize_, maximumPatientCount_, + attachment_.GetCompressedSize(), transaction.GetPublicId(patientId)); + + transaction.AddAttachment(resourceId, attachment_); + + if (IsUserContentType(attachment_.GetContentType())) + { + transaction.LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId_); + } + + transaction.GetTransactionContext().SignalAttachmentsAdded(attachment_.GetCompressedSize()); + + status_ = StoreStatus_Success; + } + } + }; + + + Operations operations(attachment, publicId, maximumStorageSize, maximumPatients); + Apply(operations); + return operations.GetStatus(); + } +} diff -r f0bdd99f3d81 -r 1d96fe7e054e OrthancServer/Sources/Database/StatelessDatabaseOperations.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Mar 09 18:24:59 2021 +0100 @@ -0,0 +1,595 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" + +#include "IDatabaseWrapper.h" +#include "../DicomInstanceOrigin.h" + +#include // TODO - REMOVE + + +namespace Orthanc +{ + class DatabaseLookup; + class ParsedDicomFile; + class ServerIndexChange; + + class StatelessDatabaseOperations : public boost::noncopyable + { + public: + typedef std::list Attachments; + typedef std::map, std::string> MetadataMap; + + class ITransactionContext : public boost::noncopyable + { + public: + virtual ~ITransactionContext() + { + } + + virtual void Commit() = 0; + + virtual int64_t GetCompressedSizeDelta() = 0; + + virtual bool IsUnstableResource(int64_t id) = 0; + + virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */, + ResourceType& remainingLevel /* out */) = 0; + + virtual void MarkAsUnstable(int64_t id, + Orthanc::ResourceType type, + const std::string& publicId) = 0; + + virtual void SignalAttachmentsAdded(uint64_t compressedSize) = 0; + + virtual void SignalChange(const ServerIndexChange& change) = 0; + }; + + + class ITransactionContextFactory : public boost::noncopyable + { + public: + virtual ~ITransactionContextFactory() + { + } + + virtual ITransactionContext* Create() = 0; + }; + + + class ReadOnlyTransaction : public boost::noncopyable + { + private: + ITransactionContext& context_; + + protected: + IDatabaseWrapper& db_; + + public: + explicit ReadOnlyTransaction(IDatabaseWrapper& db, + ITransactionContext& context) : + context_(context), + db_(db) + { + } + + ITransactionContext& GetTransactionContext() + { + return context_; + } + + /** + * Higher-level constructions + **/ + + SeriesStatus GetSeriesStatus(int64_t id, + int64_t expectedNumberOfInstances); + + + /** + * Read-only methods from "IDatabaseWrapper" + **/ + + void ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, // Can be NULL if not needed + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + { + return db_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit); + } + + void GetAllMetadata(std::map& target, + int64_t id) + { + db_.GetAllMetadata(target, id); + } + + void GetAllPublicIds(std::list& target, + ResourceType resourceType) + { + return db_.GetAllPublicIds(target, resourceType); + } + + void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) + { + return db_.GetAllPublicIds(target, resourceType, since, limit); + } + + void GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + { + db_.GetChanges(target, done, since, maxResults); + } + + void GetChildrenInternalId(std::list& target, + int64_t id) + { + db_.GetChildrenInternalId(target, id); + } + + void GetChildrenPublicId(std::list& target, + int64_t id) + { + db_.GetChildrenPublicId(target, id); + } + + void GetExportedResources(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + { + return db_.GetExportedResources(target, done, since, maxResults); + } + + void GetLastChange(std::list& target /*out*/) + { + db_.GetLastChange(target); + } + + void GetLastExportedResource(std::list& target /*out*/) + { + return db_.GetLastExportedResource(target); + } + + int64_t GetLastChangeIndex() + { + return db_.GetLastChangeIndex(); + } + + void GetMainDicomTags(DicomMap& map, + int64_t id) + { + db_.GetMainDicomTags(map, id); + } + + std::string GetPublicId(int64_t resourceId) + { + return db_.GetPublicId(resourceId); + } + + uint64_t GetResourceCount(ResourceType resourceType) + { + return db_.GetResourceCount(resourceType); + } + + ResourceType GetResourceType(int64_t resourceId) + { + return db_.GetResourceType(resourceId); + } + + uint64_t GetTotalCompressedSize() + { + return db_.GetTotalCompressedSize(); + } + + uint64_t GetTotalUncompressedSize() + { + return db_.GetTotalUncompressedSize(); + } + + bool IsProtectedPatient(int64_t internalId) + { + return db_.IsProtectedPatient(internalId); + } + + void ListAvailableAttachments(std::set& target, + int64_t id) + { + db_.ListAvailableAttachments(target, id); + } + + bool LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType) + { + return db_.LookupAttachment(attachment, id, contentType); + } + + bool LookupGlobalProperty(std::string& target, + GlobalProperty property) + { + return db_.LookupGlobalProperty(target, property); + } + + bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type) + { + return db_.LookupMetadata(target, id, type); + } + + bool LookupParent(int64_t& parentId, + int64_t resourceId) + { + return db_.LookupParent(parentId, resourceId); + } + + bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) + { + return db_.LookupResource(id, type, publicId); + } + + bool LookupResourceAndParent(int64_t& id, + ResourceType& type, + std::string& parentPublicId, + const std::string& publicId) + { + return db_.LookupResourceAndParent(id, type, parentPublicId, publicId); + } + }; + + + class ReadWriteTransaction : public ReadOnlyTransaction + { + public: + ReadWriteTransaction(IDatabaseWrapper& db, + ITransactionContext& context) : + ReadOnlyTransaction(db, context) + { + } + + void AddAttachment(int64_t id, + const FileInfo& attachment) + { + db_.AddAttachment(id, attachment); + } + + void ClearChanges() + { + db_.ClearChanges(); + } + + void ClearExportedResources() + { + db_.ClearExportedResources(); + } + + void ClearMainDicomTags(int64_t id) + { + return db_.ClearMainDicomTags(id); + } + + bool CreateInstance(IDatabaseWrapper::CreateInstanceResult& result, /* out */ + int64_t& instanceId, /* out */ + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance) + { + return db_.CreateInstance(result, instanceId, patient, study, series, instance); + } + + void DeleteAttachment(int64_t id, + FileContentType attachment) + { + return db_.DeleteAttachment(id, attachment); + } + + void DeleteMetadata(int64_t id, + MetadataType type) + { + db_.DeleteMetadata(id, type); + } + + void DeleteResource(int64_t id) + { + db_.DeleteResource(id); + } + + void LogChange(int64_t internalId, + ChangeType changeType, + ResourceType resourceType, + const std::string& publicId); + + void LogExportedResource(const ExportedResource& resource) + { + db_.LogExportedResource(resource); + } + + void SetGlobalProperty(GlobalProperty property, + const std::string& value) + { + db_.SetGlobalProperty(property, value); + } + + void SetMetadata(int64_t id, + MetadataType type, + const std::string& value) + { + return db_.SetMetadata(id, type, value); + } + + void SetProtectedPatient(int64_t internalId, + bool isProtected) + { + db_.SetProtectedPatient(internalId, isProtected); + } + + void SetResourcesContent(const ResourcesContent& content) + { + db_.SetResourcesContent(content); + } + + void Recycle(uint64_t maximumStorageSize, + unsigned int maximumPatients, + uint64_t addedInstanceSize, + const std::string& newPatientId); + }; + + + class IReadOnlyOperations : public boost::noncopyable + { + public: + virtual ~IReadOnlyOperations() + { + } + + virtual void Apply(ReadOnlyTransaction& transaction) = 0; + }; + + + class IReadWriteOperations : public boost::noncopyable + { + public: + virtual ~IReadWriteOperations() + { + } + + virtual void Apply(ReadWriteTransaction& transaction) = 0; + }; + + + private: + class MainDicomTagsRegistry; + class Transaction; + + IDatabaseWrapper& db_; + boost::mutex databaseMutex_; // TODO - REMOVE + std::unique_ptr factory_; + unsigned int maxRetries_; + std::unique_ptr mainDicomTagsRegistry_; + + void NormalizeLookup(std::vector& target, + const DatabaseLookup& source, + ResourceType level) const; + + void ApplyInternal(IReadOnlyOperations* readOperations, + IReadWriteOperations* writeOperations); + + protected: + void StandaloneRecycling(uint64_t maximumStorageSize, + unsigned int maximumPatientCount); + + public: + StatelessDatabaseOperations(IDatabaseWrapper& database); + + void SetTransactionContextFactory(ITransactionContextFactory* factory /* takes ownership */); + + // It is assumed that "GetDatabaseVersion()" can run out of a + // database transaction + unsigned int GetDatabaseVersion() + { + return db_.GetDatabaseVersion(); + } + + void Apply(IReadOnlyOperations& operations); + + void Apply(IReadWriteOperations& operations); + + bool ExpandResource(Json::Value& target, + const std::string& publicId, + ResourceType level); + + void GetAllMetadata(std::map& target, + const std::string& publicId, + ResourceType level); + + void GetAllUuids(std::list& target, + ResourceType resourceType); + + void GetAllUuids(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit); + + void 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); + + bool LookupAttachment(FileInfo& attachment, + const std::string& instancePublicId, + FileContentType contentType); + + void GetChanges(Json::Value& target, + int64_t since, + unsigned int maxResults); + + void GetLastChange(Json::Value& target); + + void GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults); + + void GetLastExportedResource(Json::Value& target); + + bool IsProtectedPatient(const std::string& publicId); + + void GetChildren(std::list& result, + const std::string& publicId); + + void GetChildInstances(std::list& result, + const std::string& publicId); + + bool LookupMetadata(std::string& target, + const std::string& publicId, + ResourceType expectedType, + MetadataType type); + + void ListAvailableAttachments(std::set& target, + const std::string& publicId, + ResourceType expectedType); + + bool LookupParent(std::string& target, + const std::string& publicId); + + void 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); + + void LookupIdentifierExact(std::vector& result, + ResourceType level, + const DicomTag& tag, + const std::string& value); + + bool LookupGlobalProperty(std::string& value, + GlobalProperty property); + + std::string GetGlobalProperty(GlobalProperty property, + const std::string& defaultValue); + + bool GetMainDicomTags(DicomMap& result, + const std::string& publicId, + ResourceType expectedType, + ResourceType levelOfInterest); + + // Only applicable at the instance level + bool GetAllMainDicomTags(DicomMap& result, + const std::string& instancePublicId); + + bool LookupResourceType(ResourceType& type, + const std::string& publicId); + + bool LookupParent(std::string& target, + const std::string& publicId, + ResourceType parentType); + + void ApplyLookupResources(std::vector& resourcesId, + std::vector* instancesId, // Can be NULL if not needed + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t limit); + + bool DeleteResource(Json::Value& target /* out */, + const std::string& uuid, + ResourceType expectedType); + + void LogExportedResource(const std::string& publicId, + const std::string& remoteModality); + + void SetProtectedPatient(const std::string& publicId, + bool isProtected); + + void SetMetadata(const std::string& publicId, + MetadataType type, + const std::string& value); + + void DeleteMetadata(const std::string& publicId, + MetadataType type); + + uint64_t IncrementGlobalSequence(GlobalProperty sequence); + + void DeleteChanges(); + + void DeleteExportedResources(); + + void SetGlobalProperty(GlobalProperty property, + const std::string& value); + + void DeleteAttachment(const std::string& publicId, + FileContentType type); + + void LogChange(ChangeType changeType, + const std::string& publicId, + ResourceType level); + + void ReconstructInstance(const ParsedDicomFile& dicom); + + StoreStatus Store(std::map& instanceMetadata, + const DicomMap& dicomSummary, + const Attachments& attachments, + const MetadataMap& metadata, + const DicomInstanceOrigin& origin, + bool overwrite, + bool hasTransferSyntax, + DicomTransferSyntax transferSyntax, + bool hasPixelDataOffset, + uint64_t pixelDataOffset, + uint64_t maximumStorageSize, + unsigned int maximumPatients); + + StoreStatus AddAttachment(const FileInfo& attachment, + const std::string& publicId, + uint64_t maximumStorageSize, + unsigned int maximumPatients); + }; +} diff -r f0bdd99f3d81 -r 1d96fe7e054e OrthancServer/Sources/ServerIndex.cpp --- a/OrthancServer/Sources/ServerIndex.cpp Tue Mar 09 16:40:38 2021 +0100 +++ b/OrthancServer/Sources/ServerIndex.cpp Tue Mar 09 18:24:59 2021 +0100 @@ -344,7 +344,7 @@ }; - class ServerIndex::Transaction + class ServerIndex::Transaction : public boost::noncopyable { private: ServerIndex& index_;