# HG changeset patch # User Sebastien Jodogne # Date 1618935089 -7200 # Node ID ee8706477b61132c53a9f751053e0b5f1cecde4f # Parent 844ec5ecb6ef1321e73985255776a63eea276a8b# Parent 88e892e25a51f682a38743b8fdad69a4a9f285ad integration db-changes->mainline diff -r 844ec5ecb6ef -r ee8706477b61 COPYING diff -r 844ec5ecb6ef -r ee8706477b61 NEWS --- a/NEWS Mon Apr 19 10:28:24 2021 +0200 +++ b/NEWS Tue Apr 20 18:11:29 2021 +0200 @@ -1,12 +1,27 @@ Pending changes in the mainline =============================== +General +------- + +* New configuration options related to multiple readers/writers: + - "DatabaseServerIdentifier" identifies the server in the DB among a pool of Orthanc servers + - "CheckRevisions" to protect against concurrent modifications of metadata and attachments + +REST API +-------- + +* API version upgraded to 12 +* "/system" reports the value of the "CheckRevisions" global option +* "/.../{id}/metadata/{name}" and "/.../{id}/attachments/{name}/..." URIs handle the + HTTP headers "If-Match", "If-None-Match" and "ETag" to cope with revisions Plugins ------- -* New functions in the SDK: - - OrthancPluginCallRestApi() +* New function in the SDK: OrthancPluginCallRestApi() +* Full refactoring of the database engine to handle multiple readers/writers, which notably + implies the handling of retries in the case of collisions Maintenance ----------- diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue Apr 20 18:11:29 2021 +0200 @@ -37,7 +37,7 @@ # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using # the mainline version of Orthanc -set(ORTHANC_API_VERSION "11") +set(ORTHANC_API_VERSION "12") ##################################################################### diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Resources/CodeGeneration/ErrorCodes.json --- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Tue Apr 20 18:11:29 2021 +0200 @@ -232,6 +232,18 @@ "HttpStatus" : 416, "Name": "BadRange", "Description": "Incorrect range request" + }, + { + "Code": 42, + "HttpStatus": 503, + "Name": "DatabaseCannotSerialize", + "Description": "Database could not serialize access due to concurrent update, the transaction should be retried" + }, + { + "Code": 43, + "HttpStatus": 409, + "Name": "Revision", + "Description": "A bad revision number was provided, which might indicate conflict between multiple writers" }, diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -420,28 +420,6 @@ #endif - static inline uint16_t GetCharValue(char c) - { - if (c >= '0' && c <= '9') - return c - '0'; - else if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - else if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - else - return 0; - } - - - static inline uint16_t GetTagValue(const char* c) - { - return ((GetCharValue(c[0]) << 12) + - (GetCharValue(c[1]) << 8) + - (GetCharValue(c[2]) << 4) + - GetCharValue(c[3])); - } - - #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void ParsedDicomFile::SendPathValue(RestApiOutput& output, const UriComponents& uri) const diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Sources/Enumerations.cpp --- a/OrthancFramework/Sources/Enumerations.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Sources/Enumerations.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -187,6 +187,12 @@ case ErrorCode_BadRange: return "Incorrect range request"; + case ErrorCode_DatabaseCannotSerialize: + return "Database could not serialize access due to concurrent update, the transaction should be retried"; + + case ErrorCode_Revision: + return "A bad revision number was provided, which might indicate conflict between multiple writers"; + case ErrorCode_SQLiteNotOpened: return "SQLite: The database is not opened"; @@ -2141,6 +2147,12 @@ case ErrorCode_BadRange: return HttpStatus_416_RequestedRangeNotSatisfiable; + case ErrorCode_DatabaseCannotSerialize: + return HttpStatus_503_ServiceUnavailable; + + case ErrorCode_Revision: + return HttpStatus_409_Conflict; + case ErrorCode_CreateDicomNotString: return HttpStatus_400_BadRequest; diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Sources/Enumerations.h --- a/OrthancFramework/Sources/Enumerations.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Sources/Enumerations.h Tue Apr 20 18:11:29 2021 +0200 @@ -135,6 +135,8 @@ ErrorCode_SslInitialization = 39 /*!< Cannot initialize SSL encryption, check out your certificates */, ErrorCode_DiscontinuedAbi = 40 /*!< Calling a function that has been removed from the Orthanc Framework */, ErrorCode_BadRange = 41 /*!< Incorrect range request */, + ErrorCode_DatabaseCannotSerialize = 42 /*!< Database could not serialize access due to concurrent update, the transaction should be retried */, + ErrorCode_Revision = 43 /*!< A bad revision number was provided, which might indicate conflict between multiple writers */, ErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, ErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, ErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp --- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -176,6 +176,21 @@ } + RestApiCallDocumentation& RestApiCallDocumentation::SetAnswerHeader(const std::string& name, + const std::string& description) + { + if (answerHeaders_.find(name) != answerHeaders_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Answer HTTP header \"" + name + "\" is already documented"); + } + else + { + answerHeaders_[name] = Parameter(Type_String, description, false); + return *this; + } + } + + void RestApiCallDocumentation::SetHttpGetSample(const std::string& url, bool isJson) { @@ -430,6 +445,20 @@ target["responses"]["200"]["content"][EnumerationToString(MimeType_PlainText)]["example"] = sampleText_; } + if (!answerHeaders_.empty()) + { + Json::Value answerHeaders = Json::objectValue; + + for (Parameters::const_iterator it = answerHeaders_.begin(); it != answerHeaders_.end(); ++it) + { + Json::Value h = Json::objectValue; + h["description"] = it->second.GetDescription(); + answerHeaders[it->first] = h; + } + + target["responses"]["200"]["headers"] = answerHeaders; + } + Json::Value parameters = Json::arrayValue; for (Parameters::const_iterator it = getArguments_.begin(); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h --- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h Tue Apr 20 18:11:29 2021 +0200 @@ -102,6 +102,7 @@ AllowedTypes answerTypes_; Parameters answerFields_; // Only if JSON object std::string answerDescription_; + Parameters answerHeaders_; bool hasSampleText_; std::string sampleText_; Json::Value sampleJson_; @@ -172,6 +173,9 @@ Type type, const std::string& description); + RestApiCallDocumentation& SetAnswerHeader(const std::string& name, + const std::string& description); + void SetHttpGetSample(const std::string& url, bool isJson); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Sources/RestApi/RestApiOutput.h --- a/OrthancFramework/Sources/RestApi/RestApiOutput.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Sources/RestApi/RestApiOutput.h Tue Apr 20 18:11:29 2021 +0200 @@ -59,6 +59,11 @@ return convertJsonToXml_; } + HttpOutput& GetLowLevelOutput() const + { + return output_; + } + void AnswerStream(IHttpStreamAnswer& stream); void AnswerJson(const Json::Value& value); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Sources/Toolbox.cpp --- a/OrthancFramework/Sources/Toolbox.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Sources/Toolbox.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -2385,6 +2385,17 @@ target = Json::writeString(builder, source); #endif } + + + void Toolbox::RemoveSurroundingQuotes(std::string& value) + { + if (!value.empty() && + value[0] == '\"' && + value[value.size() - 1] == '\"') + { + value = value.substr(1, value.size() - 2); + } + } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/Sources/Toolbox.h --- a/OrthancFramework/Sources/Toolbox.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/Sources/Toolbox.h Tue Apr 20 18:11:29 2021 +0200 @@ -280,6 +280,8 @@ static void WriteStyledJson(std::string& target, const Json::Value& source); + + static void RemoveSurroundingQuotes(std::string& value); }; } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancFramework/UnitTestsSources/FrameworkTests.cpp --- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -401,6 +401,27 @@ ASSERT_EQ("coucou", Toolbox::StripSpaces(" coucou \t \r \n ")); ASSERT_EQ("cou cou", Toolbox::StripSpaces(" cou cou \n ")); ASSERT_EQ("c", Toolbox::StripSpaces(" \n\t c\r \n ")); + + std::string s = "\" abd \""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ(" abd ", s); + + s = " \" abd \" "; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ(" \" abd \" ", s); + + s = Toolbox::StripSpaces(s); + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ(" abd ", s); + + s = "\""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("", s); + + s = "\"\""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("", s); + + s = "\"_\""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("_", s); + + s = "\"\"\""; + Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("\"", s); } TEST(Toolbox, Case) diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/CMakeLists.txt Tue Apr 20 18:11:29 2021 +0200 @@ -98,6 +98,8 @@ ${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/Database/VoidDatabaseListener.cpp ${CMAKE_SOURCE_DIR}/Sources/DicomInstanceOrigin.cpp ${CMAKE_SOURCE_DIR}/Sources/DicomInstanceToStore.cpp ${CMAKE_SOURCE_DIR}/Sources/EmbeddedResourceHttpHandler.cpp @@ -186,6 +188,7 @@ list(APPEND ORTHANC_SERVER_SOURCES ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabase.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV3.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPlugins.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsEnumerations.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsErrorDictionary.cpp diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -35,39 +35,231 @@ #include "OrthancPluginDatabase.h" #if ORTHANC_ENABLE_PLUGINS != 1 -#error The plugin support is disabled +# error The plugin support is disabled #endif #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../Sources/Database/Compatibility/ICreateInstance.h" +#include "../../Sources/Database/Compatibility/IGetChildrenMetadata.h" +#include "../../Sources/Database/Compatibility/ILookupResourceAndParent.h" +#include "../../Sources/Database/Compatibility/ILookupResources.h" +#include "../../Sources/Database/Compatibility/ISetResourcesContent.h" +#include "../../Sources/Database/VoidDatabaseListener.h" #include "PluginsEnumerations.h" #include namespace Orthanc { - class OrthancPluginDatabase::Transaction : public IDatabaseWrapper::ITransaction + class OrthancPluginDatabase::Transaction : + public IDatabaseWrapper::ITransaction, + public Compatibility::ICreateInstance, + public Compatibility::IGetChildrenMetadata, + public Compatibility::ILookupResources, + public Compatibility::ILookupResourceAndParent, + public Compatibility::ISetResourcesContent { private: - OrthancPluginDatabase& that_; + typedef std::pair AnswerResource; + typedef std::map AnswerMetadata; + + OrthancPluginDatabase& that_; + boost::recursive_mutex::scoped_lock lock_; + IDatabaseListener& listener_; + _OrthancPluginDatabaseAnswerType type_; + + std::list answerStrings_; + std::list answerInt32_; + std::list answerInt64_; + std::list answerResources_; + std::list answerAttachments_; + + DicomMap* answerDicomMap_; + std::list* answerChanges_; + std::list* answerExportedResources_; + bool* answerDone_; + bool answerDoneIgnored_; + std::list* answerMatchingResources_; + std::list* answerMatchingInstances_; + AnswerMetadata* answerMetadata_; void CheckSuccess(OrthancPluginErrorCode code) const { - if (code != OrthancPluginErrorCode_Success) + that_.CheckSuccess(code); + } + + + static FileInfo Convert(const OrthancPluginAttachment& attachment) + { + return FileInfo(attachment.uuid, + static_cast(attachment.contentType), + attachment.uncompressedSize, + attachment.uncompressedHash, + static_cast(attachment.compressionType), + attachment.compressedSize, + attachment.compressedHash); + } + + + void ResetAnswers() + { + type_ = _OrthancPluginDatabaseAnswerType_None; + + answerDicomMap_ = NULL; + answerChanges_ = NULL; + answerExportedResources_ = NULL; + answerDone_ = NULL; + answerMatchingResources_ = NULL; + answerMatchingInstances_ = NULL; + answerMetadata_ = NULL; + } + + + void ForwardAnswers(std::list& target) + { + if (type_ != _OrthancPluginDatabaseAnswerType_None && + type_ != _OrthancPluginDatabaseAnswerType_Int64) { - that_.errorDictionary_.LogError(code, true); - throw OrthancException(static_cast(code)); + throw OrthancException(ErrorCode_DatabasePlugin); + } + + target.clear(); + + if (type_ == _OrthancPluginDatabaseAnswerType_Int64) + { + for (std::list::const_iterator + it = answerInt64_.begin(); it != answerInt64_.end(); ++it) + { + target.push_back(*it); + } + } + } + + + void ForwardAnswers(std::list& target) + { + if (type_ != _OrthancPluginDatabaseAnswerType_None && + type_ != _OrthancPluginDatabaseAnswerType_String) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + target.clear(); + + if (type_ == _OrthancPluginDatabaseAnswerType_String) + { + for (std::list::const_iterator + it = answerStrings_.begin(); it != answerStrings_.end(); ++it) + { + target.push_back(*it); + } } } - public: - explicit Transaction(OrthancPluginDatabase& that) : - that_(that) + + bool ForwardSingleAnswer(std::string& target) { + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + return false; + } + else if (type_ == _OrthancPluginDatabaseAnswerType_String && + answerStrings_.size() == 1) + { + target = answerStrings_.front(); + return true; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + bool ForwardSingleAnswer(int64_t& target) + { + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + return false; + } + else if (type_ == _OrthancPluginDatabaseAnswerType_Int64 && + answerInt64_.size() == 1) + { + target = answerInt64_.front(); + return true; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } } - virtual void Begin() ORTHANC_OVERRIDE + + void ProcessEvent(const _OrthancPluginDatabaseAnswer& answer) + { + switch (answer.type) + { + case _OrthancPluginDatabaseAnswerType_DeletedAttachment: + { + const OrthancPluginAttachment& attachment = + *reinterpret_cast(answer.valueGeneric); + listener_.SignalAttachmentDeleted(Convert(attachment)); + break; + } + + case _OrthancPluginDatabaseAnswerType_RemainingAncestor: + { + ResourceType type = Plugins::Convert(static_cast(answer.valueInt32)); + listener_.SignalRemainingAncestor(type, answer.valueString); + break; + } + + case _OrthancPluginDatabaseAnswerType_DeletedResource: + { + ResourceType type = Plugins::Convert(static_cast(answer.valueInt32)); + listener_.SignalResourceDeleted(type, answer.valueString); + break; + } + + default: + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + public: + explicit Transaction(OrthancPluginDatabase& that, + IDatabaseListener& listener) : + that_(that), + lock_(that.mutex_), + listener_(listener), + type_(_OrthancPluginDatabaseAnswerType_None), + answerDoneIgnored_(false) + { + if (that_.activeTransaction_ != NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + that_.activeTransaction_ = this; + + ResetAnswers(); + } + + virtual ~Transaction() + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } + + IDatabaseListener& GetDatabaseListener() const + { + return listener_; + } + + void Begin() { CheckSuccess(that_.backend_.startTransaction(that_.payload_)); } @@ -92,7 +284,7 @@ uint64_t newDiskSize = (that_.currentDiskSize_ + diskSizeDelta); - assert(newDiskSize == that_.GetTotalCompressedSize()); + assert(newDiskSize == GetTotalCompressedSize()); CheckSuccess(that_.backend_.commitTransaction(that_.payload_)); @@ -100,19 +292,1144 @@ that_.currentDiskSize_ = newDiskSize; } } - }; + + + void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer) + { + if (answer.type == _OrthancPluginDatabaseAnswerType_None) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (answer.type == _OrthancPluginDatabaseAnswerType_DeletedAttachment || + answer.type == _OrthancPluginDatabaseAnswerType_DeletedResource || + answer.type == _OrthancPluginDatabaseAnswerType_RemainingAncestor) + { + ProcessEvent(answer); + return; + } + + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + type_ = answer.type; + + switch (type_) + { + case _OrthancPluginDatabaseAnswerType_Int32: + answerInt32_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_Int64: + answerInt64_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_Resource: + answerResources_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_Attachment: + answerAttachments_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_String: + answerStrings_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_DicomTag: + assert(answerDicomMap_ != NULL); + answerDicomMap_->Clear(); + break; + + case _OrthancPluginDatabaseAnswerType_Change: + assert(answerChanges_ != NULL); + answerChanges_->clear(); + break; + + case _OrthancPluginDatabaseAnswerType_ExportedResource: + assert(answerExportedResources_ != NULL); + answerExportedResources_->clear(); + break; + + case _OrthancPluginDatabaseAnswerType_MatchingResource: + assert(answerMatchingResources_ != NULL); + answerMatchingResources_->clear(); + + if (answerMatchingInstances_ != NULL) + { + answerMatchingInstances_->clear(); + } + + break; + + case _OrthancPluginDatabaseAnswerType_Metadata: + assert(answerMetadata_ != NULL); + answerMetadata_->clear(); + break; + + default: + throw OrthancException(ErrorCode_DatabasePlugin, + "Unhandled type of answer for custom index plugin: " + + boost::lexical_cast(answer.type)); + } + } + else if (type_ != answer.type) + { + throw OrthancException(ErrorCode_DatabasePlugin, + "Error in the plugin protocol: Cannot change the answer type"); + } + + switch (answer.type) + { + case _OrthancPluginDatabaseAnswerType_Int32: + { + answerInt32_.push_back(answer.valueInt32); + break; + } + + case _OrthancPluginDatabaseAnswerType_Int64: + { + answerInt64_.push_back(answer.valueInt64); + break; + } + + case _OrthancPluginDatabaseAnswerType_Resource: + { + OrthancPluginResourceType type = static_cast(answer.valueInt32); + answerResources_.push_back(std::make_pair(answer.valueInt64, Plugins::Convert(type))); + break; + } + + case _OrthancPluginDatabaseAnswerType_Attachment: + { + const OrthancPluginAttachment& attachment = + *reinterpret_cast(answer.valueGeneric); + + answerAttachments_.push_back(Convert(attachment)); + break; + } + + case _OrthancPluginDatabaseAnswerType_DicomTag: + { + const OrthancPluginDicomTag& tag = *reinterpret_cast(answer.valueGeneric); + assert(answerDicomMap_ != NULL); + answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value), false); + break; + } + + case _OrthancPluginDatabaseAnswerType_String: + { + if (answer.valueString == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + type_ = _OrthancPluginDatabaseAnswerType_String; + answerStrings_.clear(); + } + else if (type_ != _OrthancPluginDatabaseAnswerType_String) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + answerStrings_.push_back(std::string(answer.valueString)); + break; + } + + case _OrthancPluginDatabaseAnswerType_Change: + { + assert(answerDone_ != NULL); + if (answer.valueUint32 == 1) + { + *answerDone_ = true; + } + else if (*answerDone_) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + const OrthancPluginChange& change = + *reinterpret_cast(answer.valueGeneric); + assert(answerChanges_ != NULL); + answerChanges_->push_back + (ServerIndexChange(change.seq, + static_cast(change.changeType), + Plugins::Convert(change.resourceType), + change.publicId, + change.date)); + } + + break; + } + + case _OrthancPluginDatabaseAnswerType_ExportedResource: + { + assert(answerDone_ != NULL); + if (answer.valueUint32 == 1) + { + *answerDone_ = true; + } + else if (*answerDone_) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + const OrthancPluginExportedResource& exported = + *reinterpret_cast(answer.valueGeneric); + assert(answerExportedResources_ != NULL); + answerExportedResources_->push_back + (ExportedResource(exported.seq, + Plugins::Convert(exported.resourceType), + exported.publicId, + exported.modality, + exported.date, + exported.patientId, + exported.studyInstanceUid, + exported.seriesInstanceUid, + exported.sopInstanceUid)); + } + + break; + } + + case _OrthancPluginDatabaseAnswerType_MatchingResource: + { + const OrthancPluginMatchingResource& match = + *reinterpret_cast(answer.valueGeneric); + + if (match.resourceId == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + assert(answerMatchingResources_ != NULL); + answerMatchingResources_->push_back(match.resourceId); + + if (answerMatchingInstances_ != NULL) + { + if (match.someInstanceId == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + answerMatchingInstances_->push_back(match.someInstanceId); + } + + break; + } + + case _OrthancPluginDatabaseAnswerType_Metadata: + { + const OrthancPluginResourcesContentMetadata& metadata = + *reinterpret_cast(answer.valueGeneric); + + MetadataType type = static_cast(metadata.metadata); + + if (metadata.value == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + assert(answerMetadata_ != NULL && + answerMetadata_->find(type) == answerMetadata_->end()); + (*answerMetadata_) [type] = metadata.value; + break; + } + + default: + throw OrthancException(ErrorCode_DatabasePlugin, + "Unhandled type of answer for custom index plugin: " + + boost::lexical_cast(answer.type)); + } + } + + + // From the "ILookupResources" interface + virtual void LookupIdentifier(std::list& result, + ResourceType level, + const DicomTag& tag, + Compatibility::IdentifierConstraintType type, + const std::string& value) ORTHANC_OVERRIDE + { + if (that_.extensions_.lookupIdentifier3 == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin, + "The database plugin does not implement the mandatory LookupIdentifier3() extension"); + } + + OrthancPluginDicomTag tmp; + tmp.group = tag.GetGroup(); + tmp.element = tag.GetElement(); + tmp.value = value.c_str(); + + ResetAnswers(); + CheckSuccess(that_.extensions_.lookupIdentifier3(that_.GetContext(), that_.payload_, Plugins::Convert(level), + &tmp, Compatibility::Convert(type))); + ForwardAnswers(result); + } + + + virtual void ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) ORTHANC_OVERRIDE + { + if (that_.extensions_.lookupResources == NULL) + { + // Fallback to compatibility mode + ILookupResources::Apply + (*this, *this, resourcesId, instancesId, lookup, queryLevel, limit); + } + else + { + std::vector constraints; + std::vector< std::vector > constraintsValues; + + constraints.resize(lookup.size()); + constraintsValues.resize(lookup.size()); + + for (size_t i = 0; i < lookup.size(); i++) + { + lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]); + } + + ResetAnswers(); + answerMatchingResources_ = &resourcesId; + answerMatchingInstances_ = instancesId; + + CheckSuccess(that_.extensions_.lookupResources(that_.GetContext(), that_.payload_, lookup.size(), + (lookup.empty() ? NULL : &constraints[0]), + Plugins::Convert(queryLevel), + limit, (instancesId == NULL ? 0 : 1))); + } + } + + + virtual bool CreateInstance(IDatabaseWrapper::CreateInstanceResult& result, + int64_t& instanceId, + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance) ORTHANC_OVERRIDE + { + if (that_.extensions_.createInstance == NULL) + { + // Fallback to compatibility mode + return ICreateInstance::Apply + (*this, result, instanceId, patient, study, series, instance); + } + else + { + OrthancPluginCreateInstanceResult output; + memset(&output, 0, sizeof(output)); + + CheckSuccess(that_.extensions_.createInstance(&output, that_.payload_, patient.c_str(), + study.c_str(), series.c_str(), instance.c_str())); + + instanceId = output.instanceId; + + if (output.isNewInstance) + { + result.isNewPatient_ = output.isNewPatient; + result.isNewStudy_ = output.isNewStudy; + result.isNewSeries_ = output.isNewSeries; + result.patientId_ = output.patientId; + result.studyId_ = output.studyId; + result.seriesId_ = output.seriesId; + return true; + } + else + { + return false; + } + } + } + + + virtual void AddAttachment(int64_t id, + const FileInfo& attachment, + int64_t revision) ORTHANC_OVERRIDE + { + // "revision" is not used, as it was added in Orthanc 1.9.2 + OrthancPluginAttachment tmp; + tmp.uuid = attachment.GetUuid().c_str(); + tmp.contentType = static_cast(attachment.GetContentType()); + tmp.uncompressedSize = attachment.GetUncompressedSize(); + tmp.uncompressedHash = attachment.GetUncompressedMD5().c_str(); + tmp.compressionType = static_cast(attachment.GetCompressionType()); + tmp.compressedSize = attachment.GetCompressedSize(); + tmp.compressedHash = attachment.GetCompressedMD5().c_str(); + + CheckSuccess(that_.backend_.addAttachment(that_.payload_, id, &tmp)); + } + + + // From the "ICreateInstance" interface + virtual void AttachChild(int64_t parent, + int64_t child) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.attachChild(that_.payload_, parent, child)); + } + + + virtual void ClearChanges() ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.clearChanges(that_.payload_)); + } + + + virtual void ClearExportedResources() ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.clearExportedResources(that_.payload_)); + } + + + virtual void ClearMainDicomTags(int64_t id) ORTHANC_OVERRIDE + { + if (that_.extensions_.clearMainDicomTags == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin, + "Your custom index plugin does not implement the mandatory ClearMainDicomTags() extension"); + } + + CheckSuccess(that_.extensions_.clearMainDicomTags(that_.payload_, id)); + } + + + // From the "ICreateInstance" interface + virtual int64_t CreateResource(const std::string& publicId, + ResourceType type) ORTHANC_OVERRIDE + { + int64_t id; + CheckSuccess(that_.backend_.createResource(&id, that_.payload_, publicId.c_str(), Plugins::Convert(type))); + return id; + } + + + virtual void DeleteAttachment(int64_t id, + FileContentType attachment) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.deleteAttachment(that_.payload_, id, static_cast(attachment))); + } + + + virtual void DeleteMetadata(int64_t id, + MetadataType type) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.deleteMetadata(that_.payload_, id, static_cast(type))); + } + + + virtual void DeleteResource(int64_t id) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.deleteResource(that_.payload_, id)); + } + + + // From the "ILookupResources" interface + void GetAllInternalIds(std::list& target, + ResourceType resourceType) ORTHANC_OVERRIDE + { + if (that_.extensions_.getAllInternalIds == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin, + "The database plugin does not implement the mandatory GetAllInternalIds() extension"); + } + + ResetAnswers(); + CheckSuccess(that_.extensions_.getAllInternalIds(that_.GetContext(), that_.payload_, Plugins::Convert(resourceType))); + ForwardAnswers(target); + } + + + + virtual void GetAllMetadata(std::map& target, + int64_t id) ORTHANC_OVERRIDE + { + if (that_.extensions_.getAllMetadata == NULL) + { + // Fallback implementation if extension is missing + target.clear(); + + ResetAnswers(); + CheckSuccess(that_.backend_.listAvailableMetadata(that_.GetContext(), that_.payload_, id)); + + if (type_ != _OrthancPluginDatabaseAnswerType_None && + type_ != _OrthancPluginDatabaseAnswerType_Int32) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + target.clear(); + + if (type_ == _OrthancPluginDatabaseAnswerType_Int32) + { + for (std::list::const_iterator + it = answerInt32_.begin(); it != answerInt32_.end(); ++it) + { + MetadataType type = static_cast(*it); + + std::string value; + int64_t revision; // Ignored + if (LookupMetadata(value, revision, id, type)) + { + target[type] = value; + } + } + } + } + else + { + ResetAnswers(); + + answerMetadata_ = ⌖ + target.clear(); + + CheckSuccess(that_.extensions_.getAllMetadata(that_.GetContext(), that_.payload_, id)); + + if (type_ != _OrthancPluginDatabaseAnswerType_None && + type_ != _OrthancPluginDatabaseAnswerType_Metadata) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType) ORTHANC_OVERRIDE + { + ResetAnswers(); + CheckSuccess(that_.backend_.getAllPublicIds(that_.GetContext(), that_.payload_, Plugins::Convert(resourceType))); + ForwardAnswers(target); + } + + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) ORTHANC_OVERRIDE + { + if (that_.extensions_.getAllPublicIdsWithLimit != NULL) + { + // This extension is available since Orthanc 0.9.4 + ResetAnswers(); + CheckSuccess(that_.extensions_.getAllPublicIdsWithLimit + (that_.GetContext(), that_.payload_, Plugins::Convert(resourceType), since, limit)); + ForwardAnswers(target); + } + else + { + // The extension is not available in the database plugin, use a + // fallback implementation + target.clear(); + + if (limit == 0) + { + return; + } + + std::list tmp; + GetAllPublicIds(tmp, resourceType); + + if (tmp.size() <= since) + { + // Not enough results => empty answer + return; + } + + std::list::iterator current = tmp.begin(); + std::advance(current, since); + + while (limit > 0 && current != tmp.end()) + { + target.push_back(*current); + --limit; + ++current; + } + } + } - static FileInfo Convert(const OrthancPluginAttachment& attachment) - { - return FileInfo(attachment.uuid, - static_cast(attachment.contentType), - attachment.uncompressedSize, - attachment.uncompressedHash, - static_cast(attachment.compressionType), - attachment.compressedSize, - attachment.compressedHash); - } + virtual void GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) ORTHANC_OVERRIDE + { + ResetAnswers(); + answerChanges_ = ⌖ + answerDone_ = &done; + done = false; + + CheckSuccess(that_.backend_.getChanges(that_.GetContext(), that_.payload_, since, maxResults)); + } + + + virtual void GetChildrenInternalId(std::list& target, + int64_t id) ORTHANC_OVERRIDE + { + ResetAnswers(); + CheckSuccess(that_.backend_.getChildrenInternalId(that_.GetContext(), that_.payload_, id)); + ForwardAnswers(target); + } + + + virtual void GetChildrenMetadata(std::list& target, + int64_t resourceId, + MetadataType metadata) ORTHANC_OVERRIDE + { + if (that_.extensions_.getChildrenMetadata == NULL) + { + IGetChildrenMetadata::Apply(*this, target, resourceId, metadata); + } + else + { + ResetAnswers(); + CheckSuccess(that_.extensions_.getChildrenMetadata + (that_.GetContext(), that_.payload_, resourceId, static_cast(metadata))); + ForwardAnswers(target); + } + } + + + virtual void GetChildrenPublicId(std::list& target, + int64_t id) ORTHANC_OVERRIDE + { + ResetAnswers(); + CheckSuccess(that_.backend_.getChildrenPublicId(that_.GetContext(), that_.payload_, id)); + ForwardAnswers(target); + } + + + virtual void GetExportedResources(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) ORTHANC_OVERRIDE + { + ResetAnswers(); + answerExportedResources_ = ⌖ + answerDone_ = &done; + done = false; + + CheckSuccess(that_.backend_.getExportedResources(that_.GetContext(), that_.payload_, since, maxResults)); + } + + + virtual void GetLastChange(std::list& target /*out*/) ORTHANC_OVERRIDE + { + answerDoneIgnored_ = false; + + ResetAnswers(); + answerChanges_ = ⌖ + answerDone_ = &answerDoneIgnored_; + + CheckSuccess(that_.backend_.getLastChange(that_.GetContext(), that_.payload_)); + } + + + int64_t GetLastChangeIndex() ORTHANC_OVERRIDE + { + if (that_.extensions_.getLastChangeIndex == NULL) + { + // This was the default behavior in Orthanc <= 1.5.1 + // https://groups.google.com/d/msg/orthanc-users/QhzB6vxYeZ0/YxabgqpfBAAJ + return 0; + } + else + { + int64_t result = 0; + CheckSuccess(that_.extensions_.getLastChangeIndex(&result, that_.payload_)); + return result; + } + } + + + virtual void GetLastExportedResource(std::list& target /*out*/) ORTHANC_OVERRIDE + { + answerDoneIgnored_ = false; + + ResetAnswers(); + answerExportedResources_ = ⌖ + answerDone_ = &answerDoneIgnored_; + + CheckSuccess(that_.backend_.getLastExportedResource(that_.GetContext(), that_.payload_)); + } + + + virtual void GetMainDicomTags(DicomMap& map, + int64_t id) ORTHANC_OVERRIDE + { + ResetAnswers(); + answerDicomMap_ = ↦ + + CheckSuccess(that_.backend_.getMainDicomTags(that_.GetContext(), that_.payload_, id)); + } + + + virtual std::string GetPublicId(int64_t resourceId) ORTHANC_OVERRIDE + { + ResetAnswers(); + std::string s; + + CheckSuccess(that_.backend_.getPublicId(that_.GetContext(), that_.payload_, resourceId)); + + if (!ForwardSingleAnswer(s)) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + return s; + } + + + virtual uint64_t GetResourcesCount(ResourceType resourceType) ORTHANC_OVERRIDE + { + uint64_t count; + CheckSuccess(that_.backend_.getResourceCount(&count, that_.payload_, Plugins::Convert(resourceType))); + return count; + } + + + virtual ResourceType GetResourceType(int64_t resourceId) ORTHANC_OVERRIDE + { + OrthancPluginResourceType type; + CheckSuccess(that_.backend_.getResourceType(&type, that_.payload_, resourceId)); + return Plugins::Convert(type); + } + + + virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE + { + uint64_t size; + CheckSuccess(that_.backend_.getTotalCompressedSize(&size, that_.payload_)); + return size; + } + + + virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE + { + uint64_t size; + CheckSuccess(that_.backend_.getTotalUncompressedSize(&size, that_.payload_)); + return size; + } + + + virtual bool IsDiskSizeAbove(uint64_t threshold) ORTHANC_OVERRIDE + { + if (that_.fastGetTotalSize_) + { + return GetTotalCompressedSize() > threshold; + } + else + { + assert(GetTotalCompressedSize() == that_.currentDiskSize_); + return that_.currentDiskSize_ > threshold; + } + } + + + virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE + { + int32_t existing; + CheckSuccess(that_.backend_.isExistingResource(&existing, that_.payload_, internalId)); + return (existing != 0); + } + + + virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE + { + int32_t isProtected; + CheckSuccess(that_.backend_.isProtectedPatient(&isProtected, that_.payload_, internalId)); + return (isProtected != 0); + } + + + virtual void ListAvailableAttachments(std::set& target, + int64_t id) ORTHANC_OVERRIDE + { + ResetAnswers(); + + CheckSuccess(that_.backend_.listAvailableAttachments(that_.GetContext(), that_.payload_, id)); + + if (type_ != _OrthancPluginDatabaseAnswerType_None && + type_ != _OrthancPluginDatabaseAnswerType_Int32) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + target.clear(); + + if (type_ == _OrthancPluginDatabaseAnswerType_Int32) + { + for (std::list::const_iterator + it = answerInt32_.begin(); it != answerInt32_.end(); ++it) + { + target.insert(static_cast(*it)); + } + } + } + + + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change) ORTHANC_OVERRIDE + { + OrthancPluginChange tmp; + tmp.seq = change.GetSeq(); + tmp.changeType = static_cast(change.GetChangeType()); + tmp.resourceType = Plugins::Convert(change.GetResourceType()); + tmp.publicId = change.GetPublicId().c_str(); + tmp.date = change.GetDate().c_str(); + + CheckSuccess(that_.backend_.logChange(that_.payload_, &tmp)); + } + + + virtual void LogExportedResource(const ExportedResource& resource) ORTHANC_OVERRIDE + { + OrthancPluginExportedResource tmp; + tmp.seq = resource.GetSeq(); + tmp.resourceType = Plugins::Convert(resource.GetResourceType()); + tmp.publicId = resource.GetPublicId().c_str(); + tmp.modality = resource.GetModality().c_str(); + tmp.date = resource.GetDate().c_str(); + tmp.patientId = resource.GetPatientId().c_str(); + tmp.studyInstanceUid = resource.GetStudyInstanceUid().c_str(); + tmp.seriesInstanceUid = resource.GetSeriesInstanceUid().c_str(); + tmp.sopInstanceUid = resource.GetSopInstanceUid().c_str(); + + CheckSuccess(that_.backend_.logExportedResource(that_.payload_, &tmp)); + } + + + virtual bool LookupAttachment(FileInfo& attachment, + int64_t& revision, + int64_t id, + FileContentType contentType) ORTHANC_OVERRIDE + { + ResetAnswers(); + + CheckSuccess(that_.backend_.lookupAttachment + (that_.GetContext(), that_.payload_, id, static_cast(contentType))); + + revision = 0; // Dummy value, as revisions were added in Orthanc 1.9.2 + + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + return false; + } + else if (type_ == _OrthancPluginDatabaseAnswerType_Attachment && + answerAttachments_.size() == 1) + { + attachment = answerAttachments_.front(); + return true; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property, + bool shared) ORTHANC_OVERRIDE + { + // "shared" is unused, as database plugins using Orthanc SDK <= + // 1.9.1 are not compatible with multiple readers/writers + + ResetAnswers(); + + CheckSuccess(that_.backend_.lookupGlobalProperty + (that_.GetContext(), that_.payload_, static_cast(property))); + + return ForwardSingleAnswer(target); + } + + + // From the "ILookupResources" interface + virtual void LookupIdentifierRange(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& start, + const std::string& end) ORTHANC_OVERRIDE + { + if (that_.extensions_.lookupIdentifierRange == NULL) + { + // Default implementation, for plugins using Orthanc SDK <= 1.3.2 + + LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_GreaterOrEqual, start); + + std::list b; + LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_SmallerOrEqual, end); + + result.splice(result.end(), b); + } + else + { + ResetAnswers(); + CheckSuccess(that_.extensions_.lookupIdentifierRange(that_.GetContext(), that_.payload_, Plugins::Convert(level), + tag.GetGroup(), tag.GetElement(), + start.c_str(), end.c_str())); + ForwardAnswers(result); + } + } + + + virtual bool LookupMetadata(std::string& target, + int64_t& revision, + int64_t id, + MetadataType type) ORTHANC_OVERRIDE + { + ResetAnswers(); + CheckSuccess(that_.backend_.lookupMetadata(that_.GetContext(), that_.payload_, id, static_cast(type))); + revision = 0; // Dummy value, as revisions were added in Orthanc 1.9.2 + return ForwardSingleAnswer(target); + } + + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId) ORTHANC_OVERRIDE + { + ResetAnswers(); + CheckSuccess(that_.backend_.lookupParent(that_.GetContext(), that_.payload_, resourceId)); + return ForwardSingleAnswer(parentId); + } + + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) ORTHANC_OVERRIDE + { + ResetAnswers(); + + CheckSuccess(that_.backend_.lookupResource(that_.GetContext(), that_.payload_, publicId.c_str())); + + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + return false; + } + else if (type_ == _OrthancPluginDatabaseAnswerType_Resource && + answerResources_.size() == 1) + { + id = answerResources_.front().first; + type = answerResources_.front().second; + return true; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + virtual bool LookupResourceAndParent(int64_t& id, + ResourceType& type, + std::string& parentPublicId, + const std::string& publicId) ORTHANC_OVERRIDE + { + if (that_.extensions_.lookupResourceAndParent == NULL) + { + return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId); + } + else + { + std::list parent; + + uint8_t isExisting; + OrthancPluginResourceType pluginType = OrthancPluginResourceType_Patient; + + ResetAnswers(); + CheckSuccess(that_.extensions_.lookupResourceAndParent + (that_.GetContext(), &isExisting, &id, &pluginType, that_.payload_, publicId.c_str())); + ForwardAnswers(parent); + + if (isExisting) + { + type = Plugins::Convert(pluginType); + + if (parent.empty()) + { + if (type != ResourceType_Patient) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + else if (parent.size() == 1) + { + if ((type != ResourceType_Study && + type != ResourceType_Series && + type != ResourceType_Instance) || + parent.front().empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + parentPublicId = parent.front(); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + return true; + } + else + { + return false; + } + } + } + + + virtual bool SelectPatientToRecycle(int64_t& internalId) ORTHANC_OVERRIDE + { + ResetAnswers(); + CheckSuccess(that_.backend_.selectPatientToRecycle(that_.GetContext(), that_.payload_)); + return ForwardSingleAnswer(internalId); + } + + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) ORTHANC_OVERRIDE + { + ResetAnswers(); + CheckSuccess(that_.backend_.selectPatientToRecycle2(that_.GetContext(), that_.payload_, patientIdToAvoid)); + return ForwardSingleAnswer(internalId); + } + + + virtual void SetGlobalProperty(GlobalProperty property, + bool shared, + const std::string& value) ORTHANC_OVERRIDE + { + // "shared" is unused, as database plugins using Orthanc SDK <= + // 1.9.1 are not compatible with multiple readers/writers + + CheckSuccess(that_.backend_.setGlobalProperty + (that_.payload_, static_cast(property), value.c_str())); + } + + + // From the "ISetResourcesContent" interface + virtual void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) ORTHANC_OVERRIDE + { + OrthancPluginDicomTag tmp; + tmp.group = tag.GetGroup(); + tmp.element = tag.GetElement(); + tmp.value = value.c_str(); + + CheckSuccess(that_.backend_.setIdentifierTag(that_.payload_, id, &tmp)); + } + + + // From the "ISetResourcesContent" interface + virtual void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) ORTHANC_OVERRIDE + { + OrthancPluginDicomTag tmp; + tmp.group = tag.GetGroup(); + tmp.element = tag.GetElement(); + tmp.value = value.c_str(); + + CheckSuccess(that_.backend_.setMainDicomTag(that_.payload_, id, &tmp)); + } + + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value, + int64_t revision) ORTHANC_OVERRIDE + { + // "revision" is not used, as it was added in Orthanc 1.9.2 + CheckSuccess(that_.backend_.setMetadata + (that_.payload_, id, static_cast(type), value.c_str())); + } + + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.setProtectedPatient(that_.payload_, internalId, isProtected)); + } + + + // From the "ISetResourcesContent" interface + virtual void SetResourcesContent(const Orthanc::ResourcesContent& content) ORTHANC_OVERRIDE + { + if (that_.extensions_.setResourcesContent == NULL) + { + ISetResourcesContent::Apply(*this, content); + } + else + { + std::vector identifierTags; + std::vector mainDicomTags; + std::vector metadata; + + identifierTags.reserve(content.GetListTags().size()); + mainDicomTags.reserve(content.GetListTags().size()); + metadata.reserve(content.GetListMetadata().size()); + + for (ResourcesContent::ListTags::const_iterator + it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it) + { + OrthancPluginResourcesContentTags tmp; + tmp.resource = it->resourceId_; + tmp.group = it->tag_.GetGroup(); + tmp.element = it->tag_.GetElement(); + tmp.value = it->value_.c_str(); + + if (it->isIdentifier_) + { + identifierTags.push_back(tmp); + } + else + { + mainDicomTags.push_back(tmp); + } + } + + for (ResourcesContent::ListMetadata::const_iterator + it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it) + { + OrthancPluginResourcesContentMetadata tmp; + tmp.resource = it->resourceId_; + tmp.metadata = it->metadata_; + tmp.value = it->value_.c_str(); + metadata.push_back(tmp); + } + + assert(identifierTags.size() + mainDicomTags.size() == content.GetListTags().size() && + metadata.size() == content.GetListMetadata().size()); + + CheckSuccess(that_.extensions_.setResourcesContent( + that_.payload_, + identifierTags.size(), + (identifierTags.empty() ? NULL : &identifierTags[0]), + mainDicomTags.size(), + (mainDicomTags.empty() ? NULL : &mainDicomTags[0]), + metadata.size(), + (metadata.empty() ? NULL : &metadata[0]))); + } + } + + + // From the "ICreateInstance" interface + virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE + { + if (that_.extensions_.tagMostRecentPatient != NULL) + { + CheckSuccess(that_.extensions_.tagMostRecentPatient(that_.payload_, patient)); + } + } + }; void OrthancPluginDatabase::CheckSuccess(OrthancPluginErrorCode code) @@ -125,100 +1442,6 @@ } - void OrthancPluginDatabase::ResetAnswers() - { - type_ = _OrthancPluginDatabaseAnswerType_None; - - answerDicomMap_ = NULL; - answerChanges_ = NULL; - answerExportedResources_ = NULL; - answerDone_ = NULL; - answerMatchingResources_ = NULL; - answerMatchingInstances_ = NULL; - answerMetadata_ = NULL; - } - - - void OrthancPluginDatabase::ForwardAnswers(std::list& target) - { - if (type_ != _OrthancPluginDatabaseAnswerType_None && - type_ != _OrthancPluginDatabaseAnswerType_Int64) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - target.clear(); - - if (type_ == _OrthancPluginDatabaseAnswerType_Int64) - { - for (std::list::const_iterator - it = answerInt64_.begin(); it != answerInt64_.end(); ++it) - { - target.push_back(*it); - } - } - } - - - void OrthancPluginDatabase::ForwardAnswers(std::list& target) - { - if (type_ != _OrthancPluginDatabaseAnswerType_None && - type_ != _OrthancPluginDatabaseAnswerType_String) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - target.clear(); - - if (type_ == _OrthancPluginDatabaseAnswerType_String) - { - for (std::list::const_iterator - it = answerStrings_.begin(); it != answerStrings_.end(); ++it) - { - target.push_back(*it); - } - } - } - - - bool OrthancPluginDatabase::ForwardSingleAnswer(std::string& target) - { - if (type_ == _OrthancPluginDatabaseAnswerType_None) - { - return false; - } - else if (type_ == _OrthancPluginDatabaseAnswerType_String && - answerStrings_.size() == 1) - { - target = answerStrings_.front(); - return true; - } - else - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - } - - - bool OrthancPluginDatabase::ForwardSingleAnswer(int64_t& target) - { - if (type_ == _OrthancPluginDatabaseAnswerType_None) - { - return false; - } - else if (type_ == _OrthancPluginDatabaseAnswerType_Int64 && - answerInt64_.size() == 1) - { - target = answerInt64_.front(); - return true; - } - else - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - } - - OrthancPluginDatabase::OrthancPluginDatabase(SharedLibrary& library, PluginsErrorDictionary& errorDictionary, const OrthancPluginDatabaseBackend& backend, @@ -229,15 +1452,12 @@ errorDictionary_(errorDictionary), backend_(backend), payload_(payload), - listener_(NULL), + activeTransaction_(NULL), fastGetTotalSize_(false), - currentDiskSize_(0), - answerDoneIgnored_(false) + currentDiskSize_(0) { static const char* const MISSING = " Missing extension in database index plugin: "; - ResetAnswers(); - memset(&extensions_, 0, sizeof(extensions_)); size_t size = sizeof(extensions_); @@ -312,15 +1532,20 @@ void OrthancPluginDatabase::Open() { - CheckSuccess(backend_.open(payload_)); + { + boost::recursive_mutex::scoped_lock lock(mutex_); + CheckSuccess(backend_.open(payload_)); + } + VoidDatabaseListener listener; + { - Transaction transaction(*this); + Transaction transaction(*this, listener); transaction.Begin(); std::string tmp; fastGetTotalSize_ = - (LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) && + (transaction.LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true /* unused in old databases */) && tmp == "1"); if (fastGetTotalSize_) @@ -331,7 +1556,7 @@ { // This is the case of database plugins using Orthanc SDK <= 1.5.2 LOG(WARNING) << "Your database index plugin is not compatible with multiple Orthanc writers"; - currentDiskSize_ = GetTotalCompressedSize(); + currentDiskSize_ = transaction.GetTotalCompressedSize(); } transaction.Commit(0); @@ -339,593 +1564,21 @@ } - void OrthancPluginDatabase::AddAttachment(int64_t id, - const FileInfo& attachment) - { - OrthancPluginAttachment tmp; - tmp.uuid = attachment.GetUuid().c_str(); - tmp.contentType = static_cast(attachment.GetContentType()); - tmp.uncompressedSize = attachment.GetUncompressedSize(); - tmp.uncompressedHash = attachment.GetUncompressedMD5().c_str(); - tmp.compressionType = static_cast(attachment.GetCompressionType()); - tmp.compressedSize = attachment.GetCompressedSize(); - tmp.compressedHash = attachment.GetCompressedMD5().c_str(); - - CheckSuccess(backend_.addAttachment(payload_, id, &tmp)); - } - - - void OrthancPluginDatabase::AttachChild(int64_t parent, - int64_t child) - { - CheckSuccess(backend_.attachChild(payload_, parent, child)); - } - - - void OrthancPluginDatabase::ClearChanges() - { - CheckSuccess(backend_.clearChanges(payload_)); - } - - - void OrthancPluginDatabase::ClearExportedResources() - { - CheckSuccess(backend_.clearExportedResources(payload_)); - } - - - int64_t OrthancPluginDatabase::CreateResource(const std::string& publicId, - ResourceType type) - { - int64_t id; - CheckSuccess(backend_.createResource(&id, payload_, publicId.c_str(), Plugins::Convert(type))); - return id; - } - - - void OrthancPluginDatabase::DeleteAttachment(int64_t id, - FileContentType attachment) - { - CheckSuccess(backend_.deleteAttachment(payload_, id, static_cast(attachment))); - } - - - void OrthancPluginDatabase::DeleteMetadata(int64_t id, - MetadataType type) - { - CheckSuccess(backend_.deleteMetadata(payload_, id, static_cast(type))); - } - - - void OrthancPluginDatabase::DeleteResource(int64_t id) - { - CheckSuccess(backend_.deleteResource(payload_, id)); - } - - - void OrthancPluginDatabase::GetAllMetadata(std::map& target, - int64_t id) - { - if (extensions_.getAllMetadata == NULL) - { - // Fallback implementation if extension is missing - target.clear(); - - ResetAnswers(); - CheckSuccess(backend_.listAvailableMetadata(GetContext(), payload_, id)); - - if (type_ != _OrthancPluginDatabaseAnswerType_None && - type_ != _OrthancPluginDatabaseAnswerType_Int32) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - target.clear(); - - if (type_ == _OrthancPluginDatabaseAnswerType_Int32) - { - for (std::list::const_iterator - it = answerInt32_.begin(); it != answerInt32_.end(); ++it) - { - MetadataType type = static_cast(*it); - - std::string value; - if (LookupMetadata(value, id, type)) - { - target[type] = value; - } - } - } - } - else - { - ResetAnswers(); - - answerMetadata_ = ⌖ - target.clear(); - - CheckSuccess(extensions_.getAllMetadata(GetContext(), payload_, id)); - - if (type_ != _OrthancPluginDatabaseAnswerType_None && - type_ != _OrthancPluginDatabaseAnswerType_Metadata) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - } - } - - - void OrthancPluginDatabase::GetAllInternalIds(std::list& target, - ResourceType resourceType) - { - if (extensions_.getAllInternalIds == NULL) - { - throw OrthancException(ErrorCode_DatabasePlugin, - "The database plugin does not implement the mandatory GetAllInternalIds() extension"); - } - - ResetAnswers(); - CheckSuccess(extensions_.getAllInternalIds(GetContext(), payload_, Plugins::Convert(resourceType))); - ForwardAnswers(target); - } - - - void OrthancPluginDatabase::GetAllPublicIds(std::list& target, - ResourceType resourceType) + void OrthancPluginDatabase::Close() { - ResetAnswers(); - CheckSuccess(backend_.getAllPublicIds(GetContext(), payload_, Plugins::Convert(resourceType))); - ForwardAnswers(target); - } - - - void OrthancPluginDatabase::GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) - { - if (extensions_.getAllPublicIdsWithLimit != NULL) - { - // This extension is available since Orthanc 0.9.4 - ResetAnswers(); - CheckSuccess(extensions_.getAllPublicIdsWithLimit - (GetContext(), payload_, Plugins::Convert(resourceType), since, limit)); - ForwardAnswers(target); - } - else - { - // The extension is not available in the database plugin, use a - // fallback implementation - target.clear(); - - if (limit == 0) - { - return; - } - - std::list tmp; - GetAllPublicIds(tmp, resourceType); - - if (tmp.size() <= since) - { - // Not enough results => empty answer - return; - } - - std::list::iterator current = tmp.begin(); - std::advance(current, since); - - while (limit > 0 && current != tmp.end()) - { - target.push_back(*current); - --limit; - ++current; - } - } - } - - - - void OrthancPluginDatabase::GetChanges(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) - { - ResetAnswers(); - answerChanges_ = ⌖ - answerDone_ = &done; - done = false; - - CheckSuccess(backend_.getChanges(GetContext(), payload_, since, maxResults)); - } - - - void OrthancPluginDatabase::GetChildrenInternalId(std::list& target, - int64_t id) - { - ResetAnswers(); - CheckSuccess(backend_.getChildrenInternalId(GetContext(), payload_, id)); - ForwardAnswers(target); - } - - - void OrthancPluginDatabase::GetChildrenPublicId(std::list& target, - int64_t id) - { - ResetAnswers(); - CheckSuccess(backend_.getChildrenPublicId(GetContext(), payload_, id)); - ForwardAnswers(target); - } - - - void OrthancPluginDatabase::GetExportedResources(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) - { - ResetAnswers(); - answerExportedResources_ = ⌖ - answerDone_ = &done; - done = false; - - CheckSuccess(backend_.getExportedResources(GetContext(), payload_, since, maxResults)); - } - - - void OrthancPluginDatabase::GetLastChange(std::list& target /*out*/) - { - answerDoneIgnored_ = false; - - ResetAnswers(); - answerChanges_ = ⌖ - answerDone_ = &answerDoneIgnored_; - - CheckSuccess(backend_.getLastChange(GetContext(), payload_)); - } - - - void OrthancPluginDatabase::GetLastExportedResource(std::list& target /*out*/) - { - answerDoneIgnored_ = false; - - ResetAnswers(); - answerExportedResources_ = ⌖ - answerDone_ = &answerDoneIgnored_; - - CheckSuccess(backend_.getLastExportedResource(GetContext(), payload_)); - } - - - void OrthancPluginDatabase::GetMainDicomTags(DicomMap& map, - int64_t id) - { - ResetAnswers(); - answerDicomMap_ = ↦ - - CheckSuccess(backend_.getMainDicomTags(GetContext(), payload_, id)); - } - - - std::string OrthancPluginDatabase::GetPublicId(int64_t resourceId) - { - ResetAnswers(); - std::string s; - - CheckSuccess(backend_.getPublicId(GetContext(), payload_, resourceId)); - - if (!ForwardSingleAnswer(s)) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - return s; - } - - - uint64_t OrthancPluginDatabase::GetResourceCount(ResourceType resourceType) - { - uint64_t count; - CheckSuccess(backend_.getResourceCount(&count, payload_, Plugins::Convert(resourceType))); - return count; + boost::recursive_mutex::scoped_lock lock(mutex_); + CheckSuccess(backend_.close(payload_)); } - ResourceType OrthancPluginDatabase::GetResourceType(int64_t resourceId) - { - OrthancPluginResourceType type; - CheckSuccess(backend_.getResourceType(&type, payload_, resourceId)); - return Plugins::Convert(type); - } - - - uint64_t OrthancPluginDatabase::GetTotalCompressedSize() - { - uint64_t size; - CheckSuccess(backend_.getTotalCompressedSize(&size, payload_)); - return size; - } - - - uint64_t OrthancPluginDatabase::GetTotalUncompressedSize() - { - uint64_t size; - CheckSuccess(backend_.getTotalUncompressedSize(&size, payload_)); - return size; - } - - - bool OrthancPluginDatabase::IsExistingResource(int64_t internalId) - { - int32_t existing; - CheckSuccess(backend_.isExistingResource(&existing, payload_, internalId)); - return (existing != 0); - } - - - bool OrthancPluginDatabase::IsProtectedPatient(int64_t internalId) - { - int32_t isProtected; - CheckSuccess(backend_.isProtectedPatient(&isProtected, payload_, internalId)); - return (isProtected != 0); - } - - - void OrthancPluginDatabase::ListAvailableAttachments(std::set& target, - int64_t id) - { - ResetAnswers(); - - CheckSuccess(backend_.listAvailableAttachments(GetContext(), payload_, id)); - - if (type_ != _OrthancPluginDatabaseAnswerType_None && - type_ != _OrthancPluginDatabaseAnswerType_Int32) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - target.clear(); - - if (type_ == _OrthancPluginDatabaseAnswerType_Int32) - { - for (std::list::const_iterator - it = answerInt32_.begin(); it != answerInt32_.end(); ++it) - { - target.insert(static_cast(*it)); - } - } - } - - - void OrthancPluginDatabase::LogChange(int64_t internalId, - const ServerIndexChange& change) - { - OrthancPluginChange tmp; - tmp.seq = change.GetSeq(); - tmp.changeType = static_cast(change.GetChangeType()); - tmp.resourceType = Plugins::Convert(change.GetResourceType()); - tmp.publicId = change.GetPublicId().c_str(); - tmp.date = change.GetDate().c_str(); - - CheckSuccess(backend_.logChange(payload_, &tmp)); - } - - - void OrthancPluginDatabase::LogExportedResource(const ExportedResource& resource) - { - OrthancPluginExportedResource tmp; - tmp.seq = resource.GetSeq(); - tmp.resourceType = Plugins::Convert(resource.GetResourceType()); - tmp.publicId = resource.GetPublicId().c_str(); - tmp.modality = resource.GetModality().c_str(); - tmp.date = resource.GetDate().c_str(); - tmp.patientId = resource.GetPatientId().c_str(); - tmp.studyInstanceUid = resource.GetStudyInstanceUid().c_str(); - tmp.seriesInstanceUid = resource.GetSeriesInstanceUid().c_str(); - tmp.sopInstanceUid = resource.GetSopInstanceUid().c_str(); - - CheckSuccess(backend_.logExportedResource(payload_, &tmp)); - } - - - bool OrthancPluginDatabase::LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType) - { - ResetAnswers(); - - CheckSuccess(backend_.lookupAttachment - (GetContext(), payload_, id, static_cast(contentType))); - - if (type_ == _OrthancPluginDatabaseAnswerType_None) - { - return false; - } - else if (type_ == _OrthancPluginDatabaseAnswerType_Attachment && - answerAttachments_.size() == 1) - { - attachment = answerAttachments_.front(); - return true; - } - else - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - } - - - bool OrthancPluginDatabase::LookupGlobalProperty(std::string& target, - GlobalProperty property) - { - ResetAnswers(); - - CheckSuccess(backend_.lookupGlobalProperty - (GetContext(), payload_, static_cast(property))); - - return ForwardSingleAnswer(target); - } - - - bool OrthancPluginDatabase::LookupMetadata(std::string& target, - int64_t id, - MetadataType type) - { - ResetAnswers(); - CheckSuccess(backend_.lookupMetadata(GetContext(), payload_, id, static_cast(type))); - return ForwardSingleAnswer(target); - } - - - bool OrthancPluginDatabase::LookupParent(int64_t& parentId, - int64_t resourceId) + IDatabaseWrapper::ITransaction* OrthancPluginDatabase::StartTransaction(TransactionType type, + IDatabaseListener& listener) { - ResetAnswers(); - CheckSuccess(backend_.lookupParent(GetContext(), payload_, resourceId)); - return ForwardSingleAnswer(parentId); - } - - - bool OrthancPluginDatabase::LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId) - { - ResetAnswers(); - - CheckSuccess(backend_.lookupResource(GetContext(), payload_, publicId.c_str())); - - if (type_ == _OrthancPluginDatabaseAnswerType_None) - { - return false; - } - else if (type_ == _OrthancPluginDatabaseAnswerType_Resource && - answerResources_.size() == 1) - { - id = answerResources_.front().first; - type = answerResources_.front().second; - return true; - } - else - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - } - - - bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId) - { - ResetAnswers(); - CheckSuccess(backend_.selectPatientToRecycle(GetContext(), payload_)); - return ForwardSingleAnswer(internalId); - } - - - bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid) - { - ResetAnswers(); - CheckSuccess(backend_.selectPatientToRecycle2(GetContext(), payload_, patientIdToAvoid)); - return ForwardSingleAnswer(internalId); - } - - - void OrthancPluginDatabase::SetGlobalProperty(GlobalProperty property, - const std::string& value) - { - CheckSuccess(backend_.setGlobalProperty - (payload_, static_cast(property), value.c_str())); - } - - - void OrthancPluginDatabase::ClearMainDicomTags(int64_t id) - { - if (extensions_.clearMainDicomTags == NULL) - { - throw OrthancException(ErrorCode_DatabasePlugin, - "Your custom index plugin does not implement the mandatory ClearMainDicomTags() extension"); - } - - CheckSuccess(extensions_.clearMainDicomTags(payload_, id)); - } - + // TODO - Take advantage of "type" - void OrthancPluginDatabase::SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value) - { - OrthancPluginDicomTag tmp; - tmp.group = tag.GetGroup(); - tmp.element = tag.GetElement(); - tmp.value = value.c_str(); - - CheckSuccess(backend_.setMainDicomTag(payload_, id, &tmp)); - } - - - void OrthancPluginDatabase::SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value) - { - OrthancPluginDicomTag tmp; - tmp.group = tag.GetGroup(); - tmp.element = tag.GetElement(); - tmp.value = value.c_str(); - - CheckSuccess(backend_.setIdentifierTag(payload_, id, &tmp)); - } - - - void OrthancPluginDatabase::SetMetadata(int64_t id, - MetadataType type, - const std::string& value) - { - CheckSuccess(backend_.setMetadata - (payload_, id, static_cast(type), value.c_str())); - } - - - void OrthancPluginDatabase::SetProtectedPatient(int64_t internalId, - bool isProtected) - { - CheckSuccess(backend_.setProtectedPatient(payload_, internalId, isProtected)); - } - - - IDatabaseWrapper::ITransaction* OrthancPluginDatabase::StartTransaction() - { - return new Transaction(*this); - } - - - static void ProcessEvent(IDatabaseListener& listener, - const _OrthancPluginDatabaseAnswer& answer) - { - switch (answer.type) - { - case _OrthancPluginDatabaseAnswerType_DeletedAttachment: - { - const OrthancPluginAttachment& attachment = - *reinterpret_cast(answer.valueGeneric); - listener.SignalFileDeleted(Convert(attachment)); - break; - } - - case _OrthancPluginDatabaseAnswerType_RemainingAncestor: - { - ResourceType type = Plugins::Convert(static_cast(answer.valueInt32)); - listener.SignalRemainingAncestor(type, answer.valueString); - break; - } - - case _OrthancPluginDatabaseAnswerType_DeletedResource: - { - ResourceType type = Plugins::Convert(static_cast(answer.valueInt32)); - ServerIndexChange change(ChangeType_Deleted, type, answer.valueString); - listener.SignalChange(change); - break; - } - - default: - throw OrthancException(ErrorCode_DatabasePlugin); - } + std::unique_ptr transaction(new Transaction(*this, listener)); + transaction->Begin(); + return transaction.release(); } @@ -950,9 +1603,11 @@ void OrthancPluginDatabase::Upgrade(unsigned int targetVersion, IStorageArea& storageArea) { + VoidDatabaseListener listener; + if (extensions_.upgradeDatabase != NULL) { - Transaction transaction(*this); + Transaction transaction(*this, listener); transaction.Begin(); OrthancPluginErrorCode code = extensions_.upgradeDatabase( @@ -975,563 +1630,15 @@ void OrthancPluginDatabase::AnswerReceived(const _OrthancPluginDatabaseAnswer& answer) { - if (answer.type == _OrthancPluginDatabaseAnswerType_None) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - if (answer.type == _OrthancPluginDatabaseAnswerType_DeletedAttachment || - answer.type == _OrthancPluginDatabaseAnswerType_DeletedResource || - answer.type == _OrthancPluginDatabaseAnswerType_RemainingAncestor) - { - assert(listener_ != NULL); - ProcessEvent(*listener_, answer); - return; - } - - if (type_ == _OrthancPluginDatabaseAnswerType_None) - { - type_ = answer.type; - - switch (type_) - { - case _OrthancPluginDatabaseAnswerType_Int32: - answerInt32_.clear(); - break; - - case _OrthancPluginDatabaseAnswerType_Int64: - answerInt64_.clear(); - break; - - case _OrthancPluginDatabaseAnswerType_Resource: - answerResources_.clear(); - break; - - case _OrthancPluginDatabaseAnswerType_Attachment: - answerAttachments_.clear(); - break; - - case _OrthancPluginDatabaseAnswerType_String: - answerStrings_.clear(); - break; - - case _OrthancPluginDatabaseAnswerType_DicomTag: - assert(answerDicomMap_ != NULL); - answerDicomMap_->Clear(); - break; - - case _OrthancPluginDatabaseAnswerType_Change: - assert(answerChanges_ != NULL); - answerChanges_->clear(); - break; - - case _OrthancPluginDatabaseAnswerType_ExportedResource: - assert(answerExportedResources_ != NULL); - answerExportedResources_->clear(); - break; - - case _OrthancPluginDatabaseAnswerType_MatchingResource: - assert(answerMatchingResources_ != NULL); - answerMatchingResources_->clear(); - - if (answerMatchingInstances_ != NULL) - { - answerMatchingInstances_->clear(); - } - - break; - - case _OrthancPluginDatabaseAnswerType_Metadata: - assert(answerMetadata_ != NULL); - answerMetadata_->clear(); - break; - - default: - throw OrthancException(ErrorCode_DatabasePlugin, - "Unhandled type of answer for custom index plugin: " + - boost::lexical_cast(answer.type)); - } - } - else if (type_ != answer.type) - { - throw OrthancException(ErrorCode_DatabasePlugin, - "Error in the plugin protocol: Cannot change the answer type"); - } - - switch (answer.type) - { - case _OrthancPluginDatabaseAnswerType_Int32: - { - answerInt32_.push_back(answer.valueInt32); - break; - } - - case _OrthancPluginDatabaseAnswerType_Int64: - { - answerInt64_.push_back(answer.valueInt64); - break; - } - - case _OrthancPluginDatabaseAnswerType_Resource: - { - OrthancPluginResourceType type = static_cast(answer.valueInt32); - answerResources_.push_back(std::make_pair(answer.valueInt64, Plugins::Convert(type))); - break; - } - - case _OrthancPluginDatabaseAnswerType_Attachment: - { - const OrthancPluginAttachment& attachment = - *reinterpret_cast(answer.valueGeneric); - - answerAttachments_.push_back(Convert(attachment)); - break; - } - - case _OrthancPluginDatabaseAnswerType_DicomTag: - { - const OrthancPluginDicomTag& tag = *reinterpret_cast(answer.valueGeneric); - assert(answerDicomMap_ != NULL); - answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value), false); - break; - } - - case _OrthancPluginDatabaseAnswerType_String: - { - if (answer.valueString == NULL) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - if (type_ == _OrthancPluginDatabaseAnswerType_None) - { - type_ = _OrthancPluginDatabaseAnswerType_String; - answerStrings_.clear(); - } - else if (type_ != _OrthancPluginDatabaseAnswerType_String) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } + boost::recursive_mutex::scoped_lock lock(mutex_); - answerStrings_.push_back(std::string(answer.valueString)); - break; - } - - case _OrthancPluginDatabaseAnswerType_Change: - { - assert(answerDone_ != NULL); - if (answer.valueUint32 == 1) - { - *answerDone_ = true; - } - else if (*answerDone_) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - else - { - const OrthancPluginChange& change = - *reinterpret_cast(answer.valueGeneric); - assert(answerChanges_ != NULL); - answerChanges_->push_back - (ServerIndexChange(change.seq, - static_cast(change.changeType), - Plugins::Convert(change.resourceType), - change.publicId, - change.date)); - } - - break; - } - - case _OrthancPluginDatabaseAnswerType_ExportedResource: - { - assert(answerDone_ != NULL); - if (answer.valueUint32 == 1) - { - *answerDone_ = true; - } - else if (*answerDone_) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - else - { - const OrthancPluginExportedResource& exported = - *reinterpret_cast(answer.valueGeneric); - assert(answerExportedResources_ != NULL); - answerExportedResources_->push_back - (ExportedResource(exported.seq, - Plugins::Convert(exported.resourceType), - exported.publicId, - exported.modality, - exported.date, - exported.patientId, - exported.studyInstanceUid, - exported.seriesInstanceUid, - exported.sopInstanceUid)); - } - - break; - } - - case _OrthancPluginDatabaseAnswerType_MatchingResource: - { - const OrthancPluginMatchingResource& match = - *reinterpret_cast(answer.valueGeneric); - - if (match.resourceId == NULL) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - assert(answerMatchingResources_ != NULL); - answerMatchingResources_->push_back(match.resourceId); - - if (answerMatchingInstances_ != NULL) - { - if (match.someInstanceId == NULL) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - answerMatchingInstances_->push_back(match.someInstanceId); - } - - break; - } - - case _OrthancPluginDatabaseAnswerType_Metadata: - { - const OrthancPluginResourcesContentMetadata& metadata = - *reinterpret_cast(answer.valueGeneric); - - MetadataType type = static_cast(metadata.metadata); - - if (metadata.value == NULL) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - assert(answerMetadata_ != NULL && - answerMetadata_->find(type) == answerMetadata_->end()); - (*answerMetadata_) [type] = metadata.value; - break; - } - - default: - throw OrthancException(ErrorCode_DatabasePlugin, - "Unhandled type of answer for custom index plugin: " + - boost::lexical_cast(answer.type)); - } - } - - - bool OrthancPluginDatabase::IsDiskSizeAbove(uint64_t threshold) - { - if (fastGetTotalSize_) + if (activeTransaction_ != NULL) { - return GetTotalCompressedSize() > threshold; - } - else - { - assert(GetTotalCompressedSize() == currentDiskSize_); - return currentDiskSize_ > threshold; - } - } - - - void OrthancPluginDatabase::ApplyLookupResources(std::list& resourcesId, - std::list* instancesId, - const std::vector& lookup, - ResourceType queryLevel, - size_t limit) - { - if (extensions_.lookupResources == NULL) - { - // Fallback to compatibility mode - ILookupResources::Apply - (*this, *this, resourcesId, instancesId, lookup, queryLevel, limit); + activeTransaction_->AnswerReceived(answer); } else { - std::vector constraints; - std::vector< std::vector > constraintsValues; - - constraints.resize(lookup.size()); - constraintsValues.resize(lookup.size()); - - for (size_t i = 0; i < lookup.size(); i++) - { - lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]); - } - - ResetAnswers(); - answerMatchingResources_ = &resourcesId; - answerMatchingInstances_ = instancesId; - - CheckSuccess(extensions_.lookupResources(GetContext(), payload_, lookup.size(), - (lookup.empty() ? NULL : &constraints[0]), - Plugins::Convert(queryLevel), - limit, (instancesId == NULL ? 0 : 1))); - } - } - - - bool OrthancPluginDatabase::CreateInstance( - IDatabaseWrapper::CreateInstanceResult& result, - int64_t& instanceId, - const std::string& patient, - const std::string& study, - const std::string& series, - const std::string& instance) - { - if (extensions_.createInstance == NULL) - { - // Fallback to compatibility mode - return ICreateInstance::Apply - (*this, result, instanceId, patient, study, series, instance); - } - else - { - OrthancPluginCreateInstanceResult output; - memset(&output, 0, sizeof(output)); - - CheckSuccess(extensions_.createInstance(&output, payload_, patient.c_str(), - study.c_str(), series.c_str(), instance.c_str())); - - instanceId = output.instanceId; - - if (output.isNewInstance) - { - result.isNewPatient_ = output.isNewPatient; - result.isNewStudy_ = output.isNewStudy; - result.isNewSeries_ = output.isNewSeries; - result.patientId_ = output.patientId; - result.studyId_ = output.studyId; - result.seriesId_ = output.seriesId; - return true; - } - else - { - return false; - } - } - } - - - void OrthancPluginDatabase::LookupIdentifier(std::list& result, - ResourceType level, - const DicomTag& tag, - Compatibility::IdentifierConstraintType type, - const std::string& value) - { - if (extensions_.lookupIdentifier3 == NULL) - { - throw OrthancException(ErrorCode_DatabasePlugin, - "The database plugin does not implement the mandatory LookupIdentifier3() extension"); - } - - OrthancPluginDicomTag tmp; - tmp.group = tag.GetGroup(); - tmp.element = tag.GetElement(); - tmp.value = value.c_str(); - - ResetAnswers(); - CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level), - &tmp, Compatibility::Convert(type))); - ForwardAnswers(result); - } - - - void OrthancPluginDatabase::LookupIdentifierRange(std::list& result, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end) - { - if (extensions_.lookupIdentifierRange == NULL) - { - // Default implementation, for plugins using Orthanc SDK <= 1.3.2 - - LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_GreaterOrEqual, start); - - std::list b; - LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_SmallerOrEqual, end); - - result.splice(result.end(), b); - } - else - { - ResetAnswers(); - CheckSuccess(extensions_.lookupIdentifierRange(GetContext(), payload_, Plugins::Convert(level), - tag.GetGroup(), tag.GetElement(), - start.c_str(), end.c_str())); - ForwardAnswers(result); - } - } - - - void OrthancPluginDatabase::SetResourcesContent(const Orthanc::ResourcesContent& content) - { - if (extensions_.setResourcesContent == NULL) - { - ISetResourcesContent::Apply(*this, content); - } - else - { - std::vector identifierTags; - std::vector mainDicomTags; - std::vector metadata; - - identifierTags.reserve(content.GetListTags().size()); - mainDicomTags.reserve(content.GetListTags().size()); - metadata.reserve(content.GetListMetadata().size()); - - for (ResourcesContent::ListTags::const_iterator - it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it) - { - OrthancPluginResourcesContentTags tmp; - tmp.resource = it->resourceId_; - tmp.group = it->tag_.GetGroup(); - tmp.element = it->tag_.GetElement(); - tmp.value = it->value_.c_str(); - - if (it->isIdentifier_) - { - identifierTags.push_back(tmp); - } - else - { - mainDicomTags.push_back(tmp); - } - } - - for (ResourcesContent::ListMetadata::const_iterator - it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it) - { - OrthancPluginResourcesContentMetadata tmp; - tmp.resource = it->resourceId_; - tmp.metadata = it->metadata_; - tmp.value = it->value_.c_str(); - metadata.push_back(tmp); - } - - assert(identifierTags.size() + mainDicomTags.size() == content.GetListTags().size() && - metadata.size() == content.GetListMetadata().size()); - - CheckSuccess(extensions_.setResourcesContent( - payload_, - identifierTags.size(), - (identifierTags.empty() ? NULL : &identifierTags[0]), - mainDicomTags.size(), - (mainDicomTags.empty() ? NULL : &mainDicomTags[0]), - metadata.size(), - (metadata.empty() ? NULL : &metadata[0]))); - } - } - - - - void OrthancPluginDatabase::GetChildrenMetadata(std::list& target, - int64_t resourceId, - MetadataType metadata) - { - if (extensions_.getChildrenMetadata == NULL) - { - IGetChildrenMetadata::Apply(*this, target, resourceId, metadata); - } - else - { - ResetAnswers(); - CheckSuccess(extensions_.getChildrenMetadata - (GetContext(), payload_, resourceId, static_cast(metadata))); - ForwardAnswers(target); - } - } - - - int64_t OrthancPluginDatabase::GetLastChangeIndex() - { - if (extensions_.getLastChangeIndex == NULL) - { - // This was the default behavior in Orthanc <= 1.5.1 - // https://groups.google.com/d/msg/orthanc-users/QhzB6vxYeZ0/YxabgqpfBAAJ - return 0; - } - else - { - int64_t result = 0; - CheckSuccess(extensions_.getLastChangeIndex(&result, payload_)); - return result; - } - } - - - void OrthancPluginDatabase::TagMostRecentPatient(int64_t patient) - { - if (extensions_.tagMostRecentPatient != NULL) - { - CheckSuccess(extensions_.tagMostRecentPatient(payload_, patient)); - } - } - - - bool OrthancPluginDatabase::LookupResourceAndParent(int64_t& id, - ResourceType& type, - std::string& parentPublicId, - const std::string& publicId) - { - if (extensions_.lookupResourceAndParent == NULL) - { - return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId); - } - else - { - std::list parent; - - uint8_t isExisting; - OrthancPluginResourceType pluginType = OrthancPluginResourceType_Patient; - - ResetAnswers(); - CheckSuccess(extensions_.lookupResourceAndParent - (GetContext(), &isExisting, &id, &pluginType, payload_, publicId.c_str())); - ForwardAnswers(parent); - - if (isExisting) - { - type = Plugins::Convert(pluginType); - - if (parent.empty()) - { - if (type != ResourceType_Patient) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - } - else if (parent.size() == 1) - { - if ((type != ResourceType_Study && - type != ResourceType_Series && - type != ResourceType_Instance) || - parent.front().empty()) - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - parentPublicId = parent.front(); - } - else - { - throw OrthancException(ErrorCode_DatabasePlugin); - } - - return true; - } - else - { - return false; - } + LOG(WARNING) << "Received an answer from the database index plugin, but not transaction is active"; } } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Plugins/Engine/OrthancPluginDatabase.h --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Tue Apr 20 18:11:29 2021 +0200 @@ -36,55 +36,46 @@ #if ORTHANC_ENABLE_PLUGINS == 1 #include "../../../OrthancFramework/Sources/SharedLibrary.h" -#include "../../Sources/Database/Compatibility/ICreateInstance.h" -#include "../../Sources/Database/Compatibility/IGetChildrenMetadata.h" -#include "../../Sources/Database/Compatibility/ILookupResources.h" -#include "../../Sources/Database/Compatibility/ILookupResourceAndParent.h" -#include "../../Sources/Database/Compatibility/ISetResourcesContent.h" +#include "../../Sources/Database/IDatabaseWrapper.h" #include "../Include/orthanc/OrthancCDatabasePlugin.h" #include "PluginsErrorDictionary.h" +#include + namespace Orthanc { - class OrthancPluginDatabase : - public IDatabaseWrapper, - public Compatibility::ICreateInstance, - public Compatibility::IGetChildrenMetadata, - public Compatibility::ILookupResources, - public Compatibility::ILookupResourceAndParent, - public Compatibility::ISetResourcesContent + /** + * This class is for backward compatibility with database plugins + * that don't use the primitives introduced in Orthanc 1.9.2 to deal + * with concurrent read-only transactions. + * + * In Orthanc <= 1.9.1, Orthanc assumed that at most 1 single thread + * was accessing the database plugin at anytime, in order to match + * the SQLite model. Read-write accesses assumed the plugin to run + * the SQL statement "START TRANSACTION SERIALIZABLE" so as to be + * able to rollback the modifications. Read-only accesses didn't + * start a transaction, as they were protected by the global mutex. + **/ + class OrthancPluginDatabase : public IDatabaseWrapper { private: class Transaction; - typedef std::pair AnswerResource; - typedef std::map AnswerMetadata; - - SharedLibrary& library_; - PluginsErrorDictionary& errorDictionary_; - _OrthancPluginDatabaseAnswerType type_; - OrthancPluginDatabaseBackend backend_; + /** + * We need a "recursive_mutex" because of "AnswerReceived()" that + * is called by the "answer" primitives of the database SDK once a + * transaction is running. + **/ + boost::recursive_mutex mutex_; + + SharedLibrary& library_; + PluginsErrorDictionary& errorDictionary_; + OrthancPluginDatabaseBackend backend_; OrthancPluginDatabaseExtensions extensions_; - void* payload_; - IDatabaseListener* listener_; - - bool fastGetTotalSize_; - uint64_t currentDiskSize_; - - std::list answerStrings_; - std::list answerInt32_; - std::list answerInt64_; - std::list answerResources_; - std::list answerAttachments_; - - DicomMap* answerDicomMap_; - std::list* answerChanges_; - std::list* answerExportedResources_; - bool* answerDone_; - bool answerDoneIgnored_; - std::list* answerMatchingResources_; - std::list* answerMatchingInstances_; - AnswerMetadata* answerMetadata_; + void* payload_; + Transaction* activeTransaction_; + bool fastGetTotalSize_; + uint64_t currentDiskSize_; OrthancPluginDatabaseContext* GetContext() { @@ -93,16 +84,6 @@ void CheckSuccess(OrthancPluginErrorCode code); - void ResetAnswers(); - - void ForwardAnswers(std::list& target); - - void ForwardAnswers(std::list& target); - - bool ForwardSingleAnswer(std::string& target); - - bool ForwardSingleAnswer(int64_t& target); - public: OrthancPluginDatabase(SharedLibrary& library, PluginsErrorDictionary& errorDictionary, @@ -111,266 +92,39 @@ size_t extensionsSize, void *payload); - virtual void Open() - ORTHANC_OVERRIDE; + virtual void Open() ORTHANC_OVERRIDE; - virtual void Close() - ORTHANC_OVERRIDE - { - CheckSuccess(backend_.close(payload_)); - } + virtual void Close() ORTHANC_OVERRIDE; const SharedLibrary& GetSharedLibrary() const { return library_; } - virtual void AddAttachment(int64_t id, - const FileInfo& attachment) - ORTHANC_OVERRIDE; - - virtual void AttachChild(int64_t parent, - int64_t child) - ORTHANC_OVERRIDE; - - virtual void ClearChanges() - ORTHANC_OVERRIDE; - - virtual void ClearExportedResources() - ORTHANC_OVERRIDE; - - virtual int64_t CreateResource(const std::string& publicId, - ResourceType type) - ORTHANC_OVERRIDE; - - virtual void DeleteAttachment(int64_t id, - FileContentType attachment) - ORTHANC_OVERRIDE; - - virtual void DeleteMetadata(int64_t id, - MetadataType type) - ORTHANC_OVERRIDE; - - virtual void DeleteResource(int64_t id) - ORTHANC_OVERRIDE; - - virtual void FlushToDisk() - ORTHANC_OVERRIDE + virtual void FlushToDisk() ORTHANC_OVERRIDE { } - virtual bool HasFlushToDisk() const - ORTHANC_OVERRIDE + virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE { return false; } - virtual void GetAllMetadata(std::map& target, - int64_t id) - ORTHANC_OVERRIDE; - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType) - ORTHANC_OVERRIDE; - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) - ORTHANC_OVERRIDE; - - virtual void GetChanges(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) - ORTHANC_OVERRIDE; - - virtual void GetChildrenInternalId(std::list& target, - int64_t id) - ORTHANC_OVERRIDE; - - virtual void GetChildrenPublicId(std::list& target, - int64_t id) - ORTHANC_OVERRIDE; - - virtual void GetExportedResources(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) - ORTHANC_OVERRIDE; - - virtual void GetLastChange(std::list& target /*out*/) - ORTHANC_OVERRIDE; - - virtual void GetLastExportedResource(std::list& target /*out*/) - ORTHANC_OVERRIDE; - - virtual void GetMainDicomTags(DicomMap& map, - int64_t id) - ORTHANC_OVERRIDE; - - virtual std::string GetPublicId(int64_t resourceId) - ORTHANC_OVERRIDE; - - virtual uint64_t GetResourceCount(ResourceType resourceType) - ORTHANC_OVERRIDE; - - virtual ResourceType GetResourceType(int64_t resourceId) - ORTHANC_OVERRIDE; - - virtual uint64_t GetTotalCompressedSize() - ORTHANC_OVERRIDE; - - virtual uint64_t GetTotalUncompressedSize() - ORTHANC_OVERRIDE; - - virtual bool IsExistingResource(int64_t internalId) - ORTHANC_OVERRIDE; - - virtual bool IsProtectedPatient(int64_t internalId) - ORTHANC_OVERRIDE; - - virtual void ListAvailableAttachments(std::set& target, - int64_t id) - ORTHANC_OVERRIDE; - - virtual void LogChange(int64_t internalId, - const ServerIndexChange& change) - ORTHANC_OVERRIDE; - - virtual void LogExportedResource(const ExportedResource& resource) - ORTHANC_OVERRIDE; - - virtual bool LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType) - ORTHANC_OVERRIDE; - - virtual bool LookupGlobalProperty(std::string& target, - GlobalProperty property) - ORTHANC_OVERRIDE; - - virtual bool LookupMetadata(std::string& target, - int64_t id, - MetadataType type) - ORTHANC_OVERRIDE; - - virtual bool LookupParent(int64_t& parentId, - int64_t resourceId) - ORTHANC_OVERRIDE; - - virtual bool LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId) - ORTHANC_OVERRIDE; - - virtual bool SelectPatientToRecycle(int64_t& internalId) + virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type, + IDatabaseListener& listener) ORTHANC_OVERRIDE; - virtual bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid) - ORTHANC_OVERRIDE; - - virtual void SetGlobalProperty(GlobalProperty property, - const std::string& value) - ORTHANC_OVERRIDE; - - virtual void ClearMainDicomTags(int64_t id) - ORTHANC_OVERRIDE; - - virtual void SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value) - ORTHANC_OVERRIDE; - - virtual void SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value) - ORTHANC_OVERRIDE; - - virtual void SetMetadata(int64_t id, - MetadataType type, - const std::string& value) - ORTHANC_OVERRIDE; - - virtual void SetProtectedPatient(int64_t internalId, - bool isProtected) - ORTHANC_OVERRIDE; - - virtual IDatabaseWrapper::ITransaction* StartTransaction() - ORTHANC_OVERRIDE; - - virtual void SetListener(IDatabaseListener& listener) - ORTHANC_OVERRIDE - { - listener_ = &listener; - } - - virtual unsigned int GetDatabaseVersion() - ORTHANC_OVERRIDE; + virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) - ORTHANC_OVERRIDE; + IStorageArea& storageArea) ORTHANC_OVERRIDE; + + virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE + { + return false; // No support for revisions in old API + } void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer); - - virtual bool IsDiskSizeAbove(uint64_t threshold) - ORTHANC_OVERRIDE; - - virtual void ApplyLookupResources(std::list& resourcesId, - std::list* instancesId, - const std::vector& lookup, - ResourceType queryLevel, - size_t limit) - ORTHANC_OVERRIDE; - - virtual bool CreateInstance(CreateInstanceResult& result, - int64_t& instanceId, - const std::string& patient, - const std::string& study, - const std::string& series, - const std::string& instance) - ORTHANC_OVERRIDE; - - // From the "ILookupResources" interface - virtual void GetAllInternalIds(std::list& target, - ResourceType resourceType) - ORTHANC_OVERRIDE; - - // From the "ILookupResources" interface - virtual void LookupIdentifier(std::list& result, - ResourceType level, - const DicomTag& tag, - Compatibility::IdentifierConstraintType type, - const std::string& value) - ORTHANC_OVERRIDE; - - // From the "ILookupResources" interface - virtual void LookupIdentifierRange(std::list& result, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end) - ORTHANC_OVERRIDE; - - virtual void SetResourcesContent(const Orthanc::ResourcesContent& content) - ORTHANC_OVERRIDE; - - virtual void GetChildrenMetadata(std::list& target, - int64_t resourceId, - MetadataType metadata) - ORTHANC_OVERRIDE; - - virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE; - - virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE; - - virtual bool LookupResourceAndParent(int64_t& id, - ResourceType& type, - std::string& parentPublicId, - const std::string& publicId) - ORTHANC_OVERRIDE; }; } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -0,0 +1,1244 @@ +/** + * 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 "../../Sources/PrecompiledHeadersServer.h" +#include "OrthancPluginDatabaseV3.h" + +#if ORTHANC_ENABLE_PLUGINS != 1 +# error The plugin support is disabled +#endif + +#include "../../../OrthancFramework/Sources/Logging.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../Sources/Database/ResourcesContent.h" +#include "../../Sources/Database/VoidDatabaseListener.h" +#include "PluginsEnumerations.h" + +#include + + +#define CHECK_FUNCTION_EXISTS(backend, func) \ + if (backend.func == NULL) \ + { \ + throw OrthancException( \ + ErrorCode_DatabasePlugin, "Missing primitive: " #func "()"); \ + } + +namespace Orthanc +{ + class OrthancPluginDatabaseV3::Transaction : public IDatabaseWrapper::ITransaction + { + private: + OrthancPluginDatabaseV3& that_; + IDatabaseListener& listener_; + OrthancPluginDatabaseTransaction* transaction_; + + + void CheckSuccess(OrthancPluginErrorCode code) const + { + that_.CheckSuccess(code); + } + + + static FileInfo Convert(const OrthancPluginAttachment& attachment) + { + return FileInfo(attachment.uuid, + static_cast(attachment.contentType), + attachment.uncompressedSize, + attachment.uncompressedHash, + static_cast(attachment.compressionType), + attachment.compressedSize, + attachment.compressedHash); + } + + + void ReadStringAnswers(std::list& target) + { + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + target.clear(); + for (uint32_t i = 0; i < count; i++) + { + const char* value = NULL; + CheckSuccess(that_.backend_.readAnswerString(transaction_, &value, i)); + if (value == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + target.push_back(value); + } + } + } + + + bool ReadSingleStringAnswer(std::string& target) + { + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + if (count == 0) + { + return false; + } + else if (count == 1) + { + const char* value = NULL; + CheckSuccess(that_.backend_.readAnswerString(transaction_, &value, 0)); + if (value == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + target.assign(value); + return true; + } + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + bool ReadSingleInt64Answer(int64_t& target) + { + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + if (count == 0) + { + return false; + } + else if (count == 1) + { + CheckSuccess(that_.backend_.readAnswerInt64(transaction_, &target, 0)); + return true; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + ExportedResource ReadAnswerExportedResource(uint32_t answerIndex) + { + OrthancPluginExportedResource exported; + CheckSuccess(that_.backend_.readAnswerExportedResource(transaction_, &exported, answerIndex)); + + if (exported.publicId == NULL || + exported.modality == NULL || + exported.date == NULL || + exported.patientId == NULL || + exported.studyInstanceUid == NULL || + exported.seriesInstanceUid == NULL || + exported.sopInstanceUid == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + return ExportedResource(exported.seq, + Plugins::Convert(exported.resourceType), + exported.publicId, + exported.modality, + exported.date, + exported.patientId, + exported.studyInstanceUid, + exported.seriesInstanceUid, + exported.sopInstanceUid); + } + } + + + ServerIndexChange ReadAnswerChange(uint32_t answerIndex) + { + OrthancPluginChange change; + CheckSuccess(that_.backend_.readAnswerChange(transaction_, &change, answerIndex)); + + if (change.publicId == NULL || + change.date == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + return ServerIndexChange(change.seq, + static_cast(change.changeType), + Plugins::Convert(change.resourceType), + change.publicId, + change.date); + } + } + + + void CheckNoEvent() + { + uint32_t count; + CheckSuccess(that_.backend_.readEventsCount(transaction_, &count)); + if (count != 0) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + void ProcessEvents(bool isDeletingAttachment) + { + uint32_t count; + CheckSuccess(that_.backend_.readEventsCount(transaction_, &count)); + + for (uint32_t i = 0; i < count; i++) + { + OrthancPluginDatabaseEvent event; + CheckSuccess(that_.backend_.readEvent(transaction_, &event, i)); + + switch (event.type) + { + case OrthancPluginDatabaseEventType_DeletedAttachment: + listener_.SignalAttachmentDeleted(Convert(event.content.attachment)); + break; + + case OrthancPluginDatabaseEventType_DeletedResource: + if (isDeletingAttachment) + { + // This event should only be triggered by "DeleteResource()" + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + listener_.SignalResourceDeleted(Plugins::Convert(event.content.resource.level), event.content.resource.publicId); + } + break; + + case OrthancPluginDatabaseEventType_RemainingAncestor: + if (isDeletingAttachment) + { + // This event should only triggered by "DeleteResource()" + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + listener_.SignalRemainingAncestor(Plugins::Convert(event.content.resource.level), event.content.resource.publicId); + } + break; + + default: + break; // Unhandled event + } + } + } + + + public: + Transaction(OrthancPluginDatabaseV3& that, + IDatabaseListener& listener, + OrthancPluginDatabaseTransactionType type) : + that_(that), + listener_(listener) + { + CheckSuccess(that.backend_.startTransaction(that.database_, &transaction_, type)); + if (transaction_ == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + virtual ~Transaction() + { + OrthancPluginErrorCode code = that_.backend_.destructTransaction(transaction_); + if (code != OrthancPluginErrorCode_Success) + { + // Don't throw exception in destructors + that_.errorDictionary_.LogError(code, true); + } + } + + + virtual void Rollback() ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.rollback(transaction_)); + CheckNoEvent(); + } + + + virtual void Commit(int64_t fileSizeDelta) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.commit(transaction_, fileSizeDelta)); + CheckNoEvent(); + } + + + virtual void AddAttachment(int64_t id, + const FileInfo& attachment, + int64_t revision) ORTHANC_OVERRIDE + { + OrthancPluginAttachment tmp; + tmp.uuid = attachment.GetUuid().c_str(); + tmp.contentType = static_cast(attachment.GetContentType()); + tmp.uncompressedSize = attachment.GetUncompressedSize(); + tmp.uncompressedHash = attachment.GetUncompressedMD5().c_str(); + tmp.compressionType = static_cast(attachment.GetCompressionType()); + tmp.compressedSize = attachment.GetCompressedSize(); + tmp.compressedHash = attachment.GetCompressedMD5().c_str(); + + CheckSuccess(that_.backend_.addAttachment(transaction_, id, &tmp, revision)); + CheckNoEvent(); + } + + + virtual void ClearChanges() ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.clearChanges(transaction_)); + CheckNoEvent(); + } + + + virtual void ClearExportedResources() ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.clearExportedResources(transaction_)); + CheckNoEvent(); + } + + + virtual void DeleteAttachment(int64_t id, + FileContentType attachment) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.deleteAttachment(transaction_, id, static_cast(attachment))); + ProcessEvents(true); + } + + + virtual void DeleteMetadata(int64_t id, + MetadataType type) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.deleteMetadata(transaction_, id, static_cast(type))); + CheckNoEvent(); + } + + + virtual void DeleteResource(int64_t id) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.deleteResource(transaction_, id)); + ProcessEvents(false); + } + + + virtual void GetAllMetadata(std::map& target, + int64_t id) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getAllMetadata(transaction_, id)); + CheckNoEvent(); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + target.clear(); + for (uint32_t i = 0; i < count; i++) + { + int32_t metadata; + const char* value = NULL; + CheckSuccess(that_.backend_.readAnswerMetadata(transaction_, &metadata, &value, i)); + + if (value == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + target[static_cast(metadata)] = value; + } + } + } + + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getAllPublicIds(transaction_, Plugins::Convert(resourceType))); + CheckNoEvent(); + + ReadStringAnswers(target); + } + + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getAllPublicIdsWithLimit( + transaction_, Plugins::Convert(resourceType), + static_cast(since), static_cast(limit))); + CheckNoEvent(); + + ReadStringAnswers(target); + } + + + virtual void GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) ORTHANC_OVERRIDE + { + uint8_t tmpDone = true; + CheckSuccess(that_.backend_.getChanges(transaction_, &tmpDone, since, maxResults)); + CheckNoEvent(); + + done = (tmpDone != 0); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + target.clear(); + for (uint32_t i = 0; i < count; i++) + { + target.push_back(ReadAnswerChange(i)); + } + } + + + virtual void GetChildrenInternalId(std::list& target, + int64_t id) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getChildrenInternalId(transaction_, id)); + CheckNoEvent(); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + target.clear(); + for (uint32_t i = 0; i < count; i++) + { + int64_t value; + CheckSuccess(that_.backend_.readAnswerInt64(transaction_, &value, i)); + target.push_back(value); + } + } + + + virtual void GetChildrenPublicId(std::list& target, + int64_t id) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getChildrenPublicId(transaction_, id)); + CheckNoEvent(); + + ReadStringAnswers(target); + } + + + virtual void GetExportedResources(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) ORTHANC_OVERRIDE + { + uint8_t tmpDone = true; + CheckSuccess(that_.backend_.getExportedResources(transaction_, &tmpDone, since, maxResults)); + CheckNoEvent(); + + done = (tmpDone != 0); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + target.clear(); + for (uint32_t i = 0; i < count; i++) + { + target.push_back(ReadAnswerExportedResource(i)); + } + } + + + virtual void GetLastChange(std::list& target /*out*/) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getLastChange(transaction_)); + CheckNoEvent(); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + target.clear(); + if (count == 1) + { + target.push_back(ReadAnswerChange(0)); + } + else if (count > 1) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + virtual void GetLastExportedResource(std::list& target /*out*/) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getLastExportedResource(transaction_)); + CheckNoEvent(); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + target.clear(); + if (count == 1) + { + target.push_back(ReadAnswerExportedResource(0)); + } + else if (count > 1) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + virtual void GetMainDicomTags(DicomMap& target, + int64_t id) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getMainDicomTags(transaction_, id)); + CheckNoEvent(); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + target.Clear(); + for (uint32_t i = 0; i < count; i++) + { + uint16_t group, element; + const char* value = NULL; + CheckSuccess(that_.backend_.readAnswerDicomTag(transaction_, &group, &element, &value, i)); + + if (value == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + target.SetValue(group, element, std::string(value), false); + } + } + } + + + virtual std::string GetPublicId(int64_t resourceId) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getPublicId(transaction_, resourceId)); + CheckNoEvent(); + + std::string s; + if (ReadSingleStringAnswer(s)) + { + return s; + } + else + { + throw OrthancException(ErrorCode_InexistentItem); + } + } + + + virtual uint64_t GetResourcesCount(ResourceType resourceType) ORTHANC_OVERRIDE + { + uint64_t value; + CheckSuccess(that_.backend_.getResourcesCount(transaction_, &value, Plugins::Convert(resourceType))); + CheckNoEvent(); + return value; + } + + + virtual ResourceType GetResourceType(int64_t resourceId) ORTHANC_OVERRIDE + { + OrthancPluginResourceType type; + CheckSuccess(that_.backend_.getResourceType(transaction_, &type, resourceId)); + CheckNoEvent(); + return Plugins::Convert(type); + } + + + virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE + { + uint64_t s; + CheckSuccess(that_.backend_.getTotalCompressedSize(transaction_, &s)); + CheckNoEvent(); + return s; + } + + + virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE + { + uint64_t s; + CheckSuccess(that_.backend_.getTotalUncompressedSize(transaction_, &s)); + CheckNoEvent(); + return s; + } + + + virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE + { + uint8_t b; + CheckSuccess(that_.backend_.isExistingResource(transaction_, &b, internalId)); + CheckNoEvent(); + return (b != 0); + } + + + virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE + { + uint8_t b; + CheckSuccess(that_.backend_.isProtectedPatient(transaction_, &b, internalId)); + CheckNoEvent(); + return (b != 0); + } + + + virtual void ListAvailableAttachments(std::set& target, + int64_t id) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.listAvailableAttachments(transaction_, id)); + CheckNoEvent(); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + target.clear(); + for (uint32_t i = 0; i < count; i++) + { + int32_t value; + CheckSuccess(that_.backend_.readAnswerInt32(transaction_, &value, i)); + target.insert(static_cast(value)); + } + } + + + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.logChange(transaction_, static_cast(change.GetChangeType()), + internalId, Plugins::Convert(change.GetResourceType()), + change.GetDate().c_str())); + CheckNoEvent(); + } + + + virtual void LogExportedResource(const ExportedResource& resource) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.logExportedResource(transaction_, Plugins::Convert(resource.GetResourceType()), + resource.GetPublicId().c_str(), + resource.GetModality().c_str(), + resource.GetDate().c_str(), + resource.GetPatientId().c_str(), + resource.GetStudyInstanceUid().c_str(), + resource.GetSeriesInstanceUid().c_str(), + resource.GetSopInstanceUid().c_str())); + CheckNoEvent(); + } + + + virtual bool LookupAttachment(FileInfo& attachment, + int64_t& revision, + int64_t id, + FileContentType contentType) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.lookupAttachment(transaction_, &revision, id, static_cast(contentType))); + CheckNoEvent(); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + if (count == 0) + { + return false; + } + else if (count == 1) + { + OrthancPluginAttachment tmp; + CheckSuccess(that_.backend_.readAnswerAttachment(transaction_, &tmp, 0)); + attachment = Convert(tmp); + return true; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property, + bool shared) ORTHANC_OVERRIDE + { + const char* id = (shared ? "" : that_.serverIdentifier_.c_str()); + + CheckSuccess(that_.backend_.lookupGlobalProperty(transaction_, id, static_cast(property))); + CheckNoEvent(); + return ReadSingleStringAnswer(target); + } + + + virtual bool LookupMetadata(std::string& target, + int64_t& revision, + int64_t id, + MetadataType type) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.lookupMetadata(transaction_, &revision, id, static_cast(type))); + CheckNoEvent(); + return ReadSingleStringAnswer(target); + } + + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId) ORTHANC_OVERRIDE + { + uint8_t existing; + CheckSuccess(that_.backend_.lookupParent(transaction_, &existing, &parentId, resourceId)); + CheckNoEvent(); + return (existing != 0); + } + + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) ORTHANC_OVERRIDE + { + uint8_t existing; + OrthancPluginResourceType t; + CheckSuccess(that_.backend_.lookupResource(transaction_, &existing, &id, &t, publicId.c_str())); + CheckNoEvent(); + + if (existing == 0) + { + return false; + } + else + { + type = Plugins::Convert(t); + return true; + } + } + + + virtual bool SelectPatientToRecycle(int64_t& internalId) ORTHANC_OVERRIDE + { + uint8_t available; + CheckSuccess(that_.backend_.selectPatientToRecycle(transaction_, &available, &internalId)); + CheckNoEvent(); + return (available != 0); + } + + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) ORTHANC_OVERRIDE + { + uint8_t available; + CheckSuccess(that_.backend_.selectPatientToRecycle2(transaction_, &available, &internalId, patientIdToAvoid)); + CheckNoEvent(); + return (available != 0); + } + + + virtual void SetGlobalProperty(GlobalProperty property, + bool shared, + const std::string& value) ORTHANC_OVERRIDE + { + const char* id = (shared ? "" : that_.serverIdentifier_.c_str()); + + CheckSuccess(that_.backend_.setGlobalProperty(transaction_, id, static_cast(property), value.c_str())); + CheckNoEvent(); + } + + + virtual void ClearMainDicomTags(int64_t id) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.clearMainDicomTags(transaction_, id)); + CheckNoEvent(); + } + + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value, + int64_t revision) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.setMetadata(transaction_, id, static_cast(type), value.c_str(), revision)); + CheckNoEvent(); + } + + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.setProtectedPatient(transaction_, internalId, (isProtected ? 1 : 0))); + CheckNoEvent(); + } + + + virtual bool IsDiskSizeAbove(uint64_t threshold) ORTHANC_OVERRIDE + { + uint8_t tmp; + CheckSuccess(that_.backend_.isDiskSizeAbove(transaction_, &tmp, threshold)); + CheckNoEvent(); + return (tmp != 0); + } + + + virtual void ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, // Can be NULL if not needed + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) ORTHANC_OVERRIDE + { + std::vector constraints; + std::vector< std::vector > constraintsValues; + + constraints.resize(lookup.size()); + constraintsValues.resize(lookup.size()); + + for (size_t i = 0; i < lookup.size(); i++) + { + lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]); + } + + CheckSuccess(that_.backend_.lookupResources(transaction_, lookup.size(), + (lookup.empty() ? NULL : &constraints[0]), + Plugins::Convert(queryLevel), + limit, (instancesId == NULL ? 0 : 1))); + CheckNoEvent(); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + resourcesId.clear(); + + if (instancesId != NULL) + { + instancesId->clear(); + } + + for (uint32_t i = 0; i < count; i++) + { + OrthancPluginMatchingResource resource; + CheckSuccess(that_.backend_.readAnswerMatchingResource(transaction_, &resource, i)); + + if (resource.resourceId == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + resourcesId.push_back(resource.resourceId); + + if (instancesId != NULL) + { + if (resource.someInstanceId == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + instancesId->push_back(resource.someInstanceId); + } + } + } + } + + + virtual bool CreateInstance(CreateInstanceResult& result, /* out */ + int64_t& instanceId, /* out */ + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance) ORTHANC_OVERRIDE + { + OrthancPluginCreateInstanceResult output; + memset(&output, 0, sizeof(output)); + + CheckSuccess(that_.backend_.createInstance(transaction_, &output, patient.c_str(), + study.c_str(), series.c_str(), instance.c_str())); + CheckNoEvent(); + + instanceId = output.instanceId; + + if (output.isNewInstance) + { + result.isNewPatient_ = output.isNewPatient; + result.isNewStudy_ = output.isNewStudy; + result.isNewSeries_ = output.isNewSeries; + result.patientId_ = output.patientId; + result.studyId_ = output.studyId; + result.seriesId_ = output.seriesId; + return true; + } + else + { + return false; + } + + } + + + virtual void SetResourcesContent(const ResourcesContent& content) ORTHANC_OVERRIDE + { + std::vector identifierTags; + std::vector mainDicomTags; + std::vector metadata; + + identifierTags.reserve(content.GetListTags().size()); + mainDicomTags.reserve(content.GetListTags().size()); + metadata.reserve(content.GetListMetadata().size()); + + for (ResourcesContent::ListTags::const_iterator + it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it) + { + OrthancPluginResourcesContentTags tmp; + tmp.resource = it->resourceId_; + tmp.group = it->tag_.GetGroup(); + tmp.element = it->tag_.GetElement(); + tmp.value = it->value_.c_str(); + + if (it->isIdentifier_) + { + identifierTags.push_back(tmp); + } + else + { + mainDicomTags.push_back(tmp); + } + } + + for (ResourcesContent::ListMetadata::const_iterator + it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it) + { + OrthancPluginResourcesContentMetadata tmp; + tmp.resource = it->resourceId_; + tmp.metadata = it->metadata_; + tmp.value = it->value_.c_str(); + metadata.push_back(tmp); + } + + assert(identifierTags.size() + mainDicomTags.size() == content.GetListTags().size() && + metadata.size() == content.GetListMetadata().size()); + + CheckSuccess(that_.backend_.setResourcesContent(transaction_, + identifierTags.size(), + (identifierTags.empty() ? NULL : &identifierTags[0]), + mainDicomTags.size(), + (mainDicomTags.empty() ? NULL : &mainDicomTags[0]), + metadata.size(), + (metadata.empty() ? NULL : &metadata[0]))); + CheckNoEvent(); + } + + + virtual void GetChildrenMetadata(std::list& target, + int64_t resourceId, + MetadataType metadata) ORTHANC_OVERRIDE + { + CheckSuccess(that_.backend_.getChildrenMetadata(transaction_, resourceId, static_cast(metadata))); + CheckNoEvent(); + ReadStringAnswers(target); + } + + + virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE + { + int64_t tmp; + CheckSuccess(that_.backend_.getLastChangeIndex(transaction_, &tmp)); + CheckNoEvent(); + return tmp; + } + + + virtual bool LookupResourceAndParent(int64_t& id, + ResourceType& type, + std::string& parentPublicId, + const std::string& publicId) ORTHANC_OVERRIDE + { + uint8_t isExisting; + OrthancPluginResourceType tmpType; + CheckSuccess(that_.backend_.lookupResourceAndParent(transaction_, &isExisting, &id, &tmpType, publicId.c_str())); + CheckNoEvent(); + + if (isExisting) + { + type = Plugins::Convert(tmpType); + + uint32_t count; + CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count)); + + if (count > 1) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + switch (type) + { + case ResourceType_Patient: + // A patient has no parent + if (count == 1) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + break; + + case ResourceType_Study: + case ResourceType_Series: + case ResourceType_Instance: + if (count == 0) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + const char* value = NULL; + CheckSuccess(that_.backend_.readAnswerString(transaction_, &value, 0)); + if (value == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + parentPublicId.assign(value); + } + } + break; + + default: + throw OrthancException(ErrorCode_DatabasePlugin); + } + + return true; + } + else + { + return false; + } + } + }; + + + void OrthancPluginDatabaseV3::CheckSuccess(OrthancPluginErrorCode code) const + { + if (code != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + + OrthancPluginDatabaseV3::OrthancPluginDatabaseV3(SharedLibrary& library, + PluginsErrorDictionary& errorDictionary, + const OrthancPluginDatabaseBackendV3* backend, + size_t backendSize, + void* database, + const std::string& serverIdentifier) : + library_(library), + errorDictionary_(errorDictionary), + database_(database), + serverIdentifier_(serverIdentifier) + { + CLOG(INFO, PLUGINS) << "Identifier of this Orthanc server for the global properties " + << "of the custom database: \"" << serverIdentifier << "\""; + + if (backendSize >= sizeof(backend_)) + { + memcpy(&backend_, backend, sizeof(backend_)); + } + else + { + // Not all the primitives are implemented by the plugin + memset(&backend_, 0, sizeof(backend_)); + memcpy(&backend_, backend, backendSize); + } + + // Sanity checks + CHECK_FUNCTION_EXISTS(backend_, readAnswersCount); + CHECK_FUNCTION_EXISTS(backend_, readAnswerAttachment); + CHECK_FUNCTION_EXISTS(backend_, readAnswerChange); + CHECK_FUNCTION_EXISTS(backend_, readAnswerDicomTag); + CHECK_FUNCTION_EXISTS(backend_, readAnswerExportedResource); + CHECK_FUNCTION_EXISTS(backend_, readAnswerInt32); + CHECK_FUNCTION_EXISTS(backend_, readAnswerInt64); + CHECK_FUNCTION_EXISTS(backend_, readAnswerMatchingResource); + CHECK_FUNCTION_EXISTS(backend_, readAnswerMetadata); + CHECK_FUNCTION_EXISTS(backend_, readAnswerString); + + CHECK_FUNCTION_EXISTS(backend_, readEventsCount); + CHECK_FUNCTION_EXISTS(backend_, readEvent); + + CHECK_FUNCTION_EXISTS(backend_, open); + CHECK_FUNCTION_EXISTS(backend_, close); + CHECK_FUNCTION_EXISTS(backend_, destructDatabase); + CHECK_FUNCTION_EXISTS(backend_, getDatabaseVersion); + CHECK_FUNCTION_EXISTS(backend_, upgradeDatabase); + CHECK_FUNCTION_EXISTS(backend_, startTransaction); + CHECK_FUNCTION_EXISTS(backend_, destructTransaction); + CHECK_FUNCTION_EXISTS(backend_, hasRevisionsSupport); + + CHECK_FUNCTION_EXISTS(backend_, rollback); + CHECK_FUNCTION_EXISTS(backend_, commit); + + CHECK_FUNCTION_EXISTS(backend_, addAttachment); + CHECK_FUNCTION_EXISTS(backend_, clearChanges); + CHECK_FUNCTION_EXISTS(backend_, clearExportedResources); + CHECK_FUNCTION_EXISTS(backend_, clearMainDicomTags); + CHECK_FUNCTION_EXISTS(backend_, createInstance); + CHECK_FUNCTION_EXISTS(backend_, deleteAttachment); + CHECK_FUNCTION_EXISTS(backend_, deleteMetadata); + CHECK_FUNCTION_EXISTS(backend_, deleteResource); + CHECK_FUNCTION_EXISTS(backend_, getAllMetadata); + CHECK_FUNCTION_EXISTS(backend_, getAllPublicIds); + CHECK_FUNCTION_EXISTS(backend_, getAllPublicIdsWithLimit); + CHECK_FUNCTION_EXISTS(backend_, getChanges); + CHECK_FUNCTION_EXISTS(backend_, getChildrenInternalId); + CHECK_FUNCTION_EXISTS(backend_, getChildrenMetadata); + CHECK_FUNCTION_EXISTS(backend_, getChildrenPublicId); + CHECK_FUNCTION_EXISTS(backend_, getExportedResources); + CHECK_FUNCTION_EXISTS(backend_, getLastChange); + CHECK_FUNCTION_EXISTS(backend_, getLastChangeIndex); + CHECK_FUNCTION_EXISTS(backend_, getLastExportedResource); + CHECK_FUNCTION_EXISTS(backend_, getMainDicomTags); + CHECK_FUNCTION_EXISTS(backend_, getPublicId); + CHECK_FUNCTION_EXISTS(backend_, getResourceType); + CHECK_FUNCTION_EXISTS(backend_, getResourcesCount); + CHECK_FUNCTION_EXISTS(backend_, getTotalCompressedSize); + CHECK_FUNCTION_EXISTS(backend_, getTotalUncompressedSize); + CHECK_FUNCTION_EXISTS(backend_, isDiskSizeAbove); + CHECK_FUNCTION_EXISTS(backend_, isExistingResource); + CHECK_FUNCTION_EXISTS(backend_, isProtectedPatient); + CHECK_FUNCTION_EXISTS(backend_, listAvailableAttachments); + CHECK_FUNCTION_EXISTS(backend_, logChange); + CHECK_FUNCTION_EXISTS(backend_, logExportedResource); + CHECK_FUNCTION_EXISTS(backend_, lookupAttachment); + CHECK_FUNCTION_EXISTS(backend_, lookupGlobalProperty); + CHECK_FUNCTION_EXISTS(backend_, lookupMetadata); + CHECK_FUNCTION_EXISTS(backend_, lookupParent); + CHECK_FUNCTION_EXISTS(backend_, lookupResource); + CHECK_FUNCTION_EXISTS(backend_, lookupResourceAndParent); + CHECK_FUNCTION_EXISTS(backend_, lookupResources); + CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle); + CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle2); + CHECK_FUNCTION_EXISTS(backend_, setGlobalProperty); + CHECK_FUNCTION_EXISTS(backend_, setMetadata); + CHECK_FUNCTION_EXISTS(backend_, setProtectedPatient); + CHECK_FUNCTION_EXISTS(backend_, setResourcesContent); + } + + + OrthancPluginDatabaseV3::~OrthancPluginDatabaseV3() + { + if (database_ != NULL) + { + OrthancPluginErrorCode code = backend_.destructDatabase(database_); + if (code != OrthancPluginErrorCode_Success) + { + // Don't throw exception in destructors + errorDictionary_.LogError(code, true); + } + } + } + + + void OrthancPluginDatabaseV3::Open() + { + CheckSuccess(backend_.open(database_)); + } + + + void OrthancPluginDatabaseV3::Close() + { + CheckSuccess(backend_.close(database_)); + } + + + IDatabaseWrapper::ITransaction* OrthancPluginDatabaseV3::StartTransaction(TransactionType type, + IDatabaseListener& listener) + { + switch (type) + { + case TransactionType_ReadOnly: + return new Transaction(*this, listener, OrthancPluginDatabaseTransactionType_ReadOnly); + + case TransactionType_ReadWrite: + return new Transaction(*this, listener, OrthancPluginDatabaseTransactionType_ReadWrite); + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + unsigned int OrthancPluginDatabaseV3::GetDatabaseVersion() + { + uint32_t version = 0; + CheckSuccess(backend_.getDatabaseVersion(database_, &version)); + return version; + } + + + void OrthancPluginDatabaseV3::Upgrade(unsigned int targetVersion, + IStorageArea& storageArea) + { + VoidDatabaseListener listener; + + if (backend_.upgradeDatabase != NULL) + { + Transaction transaction(*this, listener, OrthancPluginDatabaseTransactionType_ReadWrite); + + OrthancPluginErrorCode code = backend_.upgradeDatabase( + database_, reinterpret_cast(&storageArea), + static_cast(targetVersion)); + + if (code == OrthancPluginErrorCode_Success) + { + transaction.Commit(0); + } + else + { + transaction.Rollback(); + errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + } + + + bool OrthancPluginDatabaseV3::HasRevisionsSupport() const + { + // WARNING: This method requires "Open()" to have been called + uint8_t hasRevisions; + CheckSuccess(backend_.hasRevisionsSupport(database_, &hasRevisions)); + return (hasRevisions != 0); + } +} diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Tue Apr 20 18:11:29 2021 +0200 @@ -0,0 +1,99 @@ +/** + * 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 + +#if ORTHANC_ENABLE_PLUGINS == 1 + +#include "../../../OrthancFramework/Sources/SharedLibrary.h" +#include "../../Sources/Database/IDatabaseWrapper.h" +#include "../Include/orthanc/OrthancCDatabasePlugin.h" +#include "PluginsErrorDictionary.h" + +namespace Orthanc +{ + class OrthancPluginDatabaseV3 : public IDatabaseWrapper + { + private: + class Transaction; + + SharedLibrary& library_; + PluginsErrorDictionary& errorDictionary_; + OrthancPluginDatabaseBackendV3 backend_; + void* database_; + std::string serverIdentifier_; + + void CheckSuccess(OrthancPluginErrorCode code) const; + + public: + OrthancPluginDatabaseV3(SharedLibrary& library, + PluginsErrorDictionary& errorDictionary, + const OrthancPluginDatabaseBackendV3* backend, + size_t backendSize, + void* database, + const std::string& serverIdentifier); + + virtual ~OrthancPluginDatabaseV3(); + + virtual void Open() ORTHANC_OVERRIDE; + + virtual void Close() ORTHANC_OVERRIDE; + + const SharedLibrary& GetSharedLibrary() const + { + return library_; + } + + virtual void FlushToDisk() ORTHANC_OVERRIDE + { + } + + virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE + { + return false; + } + + virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type, + IDatabaseListener& listener) + ORTHANC_OVERRIDE; + + virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; + + virtual void Upgrade(unsigned int targetVersion, + IStorageArea& storageArea) ORTHANC_OVERRIDE; + + virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE; + }; +} + +#endif diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Plugins/Engine/OrthancPlugins.cpp --- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -65,11 +65,14 @@ #include "../../../OrthancFramework/Sources/SerializationToolbox.h" #include "../../../OrthancFramework/Sources/StringMemoryBuffer.h" #include "../../../OrthancFramework/Sources/Toolbox.h" +#include "../../Sources/Database/VoidDatabaseListener.h" #include "../../Sources/OrthancConfiguration.h" #include "../../Sources/OrthancFindRequestHandler.h" #include "../../Sources/Search/HierarchicalMatcher.h" #include "../../Sources/ServerContext.h" #include "../../Sources/ServerToolbox.h" +#include "OrthancPluginDatabase.h" +#include "OrthancPluginDatabaseV3.h" #include "PluginsEnumerations.h" #include "PluginsJob.h" @@ -1206,14 +1209,19 @@ int argc_; char** argv_; std::unique_ptr database_; + std::unique_ptr databaseV3_; // New in Orthanc 1.9.2 PluginsErrorDictionary dictionary_; - - PImpl() : + std::string databaseServerIdentifier_; // New in Orthanc 1.9.2 + unsigned int maxDatabaseRetries_; // New in Orthanc 1.9.2 + + explicit PImpl(const std::string& databaseServerIdentifier) : context_(NULL), findCallback_(NULL), worklistCallback_(NULL), argc_(1), - argv_(NULL) + argv_(NULL), + databaseServerIdentifier_(databaseServerIdentifier), + maxDatabaseRetries_(0) { memset(&moveCallbacks_, 0, sizeof(moveCallbacks_)); } @@ -1687,7 +1695,7 @@ }; - OrthancPlugins::OrthancPlugins() + OrthancPlugins::OrthancPlugins(const std::string& databaseServerIdentifier) { /* Sanity check of the compiler */ if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) || @@ -1727,7 +1735,7 @@ throw OrthancException(ErrorCode_Plugin); } - pimpl_.reset(new PImpl()); + pimpl_.reset(new PImpl(databaseServerIdentifier)); pimpl_->manager_.RegisterServiceProvider(*this); } @@ -4206,8 +4214,11 @@ } else { + // TODO - Plugins can only access global properties of their + // own Orthanc server (no access to the shared global properties) PImpl::ServerContextLock lock(*pimpl_); - lock.GetContext().GetIndex().SetGlobalProperty(static_cast(p.property), p.value); + lock.GetContext().GetIndex().SetGlobalProperty(static_cast(p.property), + false /* not shared */, p.value); return true; } } @@ -4220,8 +4231,11 @@ std::string result; { + // TODO - Plugins can only access global properties of their + // own Orthanc server (no access to the shared global properties) PImpl::ServerContextLock lock(*pimpl_); - result = lock.GetContext().GetIndex().GetGlobalProperty(static_cast(p.property), p.value); + result = lock.GetContext().GetIndex().GetGlobalProperty(static_cast(p.property), + false /* not shared */, p.value); } *(p.result) = CopyString(result); @@ -5018,12 +5032,14 @@ case _OrthancPluginService_RegisterDatabaseBackend: { - CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end"; + LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API"; + LOG(WARNING) << "The database backend has *no* support for revisions of metadata and attachments"; const _OrthancPluginRegisterDatabaseBackend& p = *reinterpret_cast(parameters); - if (pimpl_->database_.get() == NULL) + if (pimpl_->database_.get() == NULL && + pimpl_->databaseV3_.get() == NULL) { pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), *p.backend, NULL, 0, p.payload)); @@ -5040,12 +5056,14 @@ case _OrthancPluginService_RegisterDatabaseBackendV2: { - CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end"; + LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API"; + LOG(WARNING) << "The database backend has *no* support for revisions of metadata and attachments"; const _OrthancPluginRegisterDatabaseBackendV2& p = *reinterpret_cast(parameters); - if (pimpl_->database_.get() == NULL) + if (pimpl_->database_.get() == NULL && + pimpl_->databaseV3_.get() == NULL) { pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), *p.backend, p.extensions, @@ -5061,6 +5079,28 @@ return true; } + case _OrthancPluginService_RegisterDatabaseBackendV3: + { + CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end"; + + const _OrthancPluginRegisterDatabaseBackendV3& p = + *reinterpret_cast(parameters); + + if (pimpl_->database_.get() == NULL && + pimpl_->databaseV3_.get() == NULL) + { + pimpl_->databaseV3_.reset(new OrthancPluginDatabaseV3(plugin, GetErrorDictionary(), p.backend, + p.backendSize, p.database, pimpl_->databaseServerIdentifier_)); + pimpl_->maxDatabaseRetries_ = p.maxDatabaseRetries; + } + else + { + throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered); + } + + return true; + } + case _OrthancPluginService_DatabaseAnswer: throw OrthancException(ErrorCode_InternalError); // Implemented before locking (*) @@ -5103,8 +5143,16 @@ "The service ReconstructMainDicomTags can only be invoked by custom database plugins"); } - IStorageArea& storage = *reinterpret_cast(p.storageArea); - ServerToolbox::ReconstructMainDicomTags(*pimpl_->database_, storage, Plugins::Convert(p.level)); + VoidDatabaseListener listener; + + { + IStorageArea& storage = *reinterpret_cast(p.storageArea); + + std::unique_ptr transaction( + pimpl_->database_->StartTransaction(TransactionType_ReadWrite, listener)); + ServerToolbox::ReconstructMainDicomTags(*transaction, storage, Plugins::Convert(p.level)); + transaction->Commit(0); + } return true; } @@ -5172,7 +5220,8 @@ bool OrthancPlugins::HasDatabaseBackend() const { boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); - return pimpl_->database_.get() != NULL; + return (pimpl_->database_.get() != NULL || + pimpl_->databaseV3_.get() != NULL); } @@ -5204,27 +5253,35 @@ IDatabaseWrapper& OrthancPlugins::GetDatabaseBackend() { - if (!HasDatabaseBackend()) + if (pimpl_->database_.get() != NULL) + { + return *pimpl_->database_; + } + else if (pimpl_->databaseV3_.get() != NULL) + { + return *pimpl_->databaseV3_; + } + else { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else - { - return *pimpl_->database_; - } } const SharedLibrary& OrthancPlugins::GetDatabaseBackendLibrary() const { - if (!HasDatabaseBackend()) + if (pimpl_->database_.get() != NULL) + { + return pimpl_->database_->GetSharedLibrary(); + } + else if (pimpl_->databaseV3_.get() != NULL) + { + return pimpl_->databaseV3_->GetSharedLibrary(); + } + else { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else - { - return pimpl_->database_->GetSharedLibrary(); - } } @@ -5702,4 +5759,11 @@ boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); return (pimpl_->authorizationTokens_.find(token) != pimpl_->authorizationTokens_.end()); } + + + unsigned int OrthancPlugins::GetMaxDatabaseRetries() const + { + boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); + return pimpl_->maxDatabaseRetries_; + } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Plugins/Engine/OrthancPlugins.h --- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Tue Apr 20 18:11:29 2021 +0200 @@ -61,10 +61,10 @@ #include "../../../OrthancFramework/Sources/HttpServer/IHttpHandler.h" #include "../../../OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h" #include "../../../OrthancFramework/Sources/JobsEngine/IJob.h" +#include "../../Sources/Database/IDatabaseWrapper.h" #include "../../Sources/IDicomImageDecoder.h" #include "../../Sources/IServerListener.h" #include "../../Sources/ServerJobs/IStorageCommitmentFactory.h" -#include "OrthancPluginDatabase.h" #include "PluginsManager.h" #include @@ -247,7 +247,7 @@ bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; public: - OrthancPlugins(); + explicit OrthancPlugins(const std::string& databaseServerIdentifier); virtual ~OrthancPlugins(); @@ -389,6 +389,8 @@ // New in Orthanc 1.8.1 (cf. "OrthancPluginGenerateRestApiAuthorizationToken()") bool IsValidAuthorizationToken(const std::string& token) const; + + unsigned int GetMaxDatabaseRetries() const; }; } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Tue Apr 20 18:11:29 2021 +0200 @@ -44,6 +44,14 @@ typedef struct _OrthancPluginDatabaseContext_t OrthancPluginDatabaseContext; + /** + * Opaque structure that represents a transaction of a custom database engine. + * New in Orthanc 1.9.2. + * @ingroup Callbacks + **/ + typedef struct _OrthancPluginDatabaseTransaction_t OrthancPluginDatabaseTransaction; + + /*InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, ¶ms); + } + #ifdef __cplusplus } #endif diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue Apr 20 18:11:29 2021 +0200 @@ -17,7 +17,7 @@ * - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback(). * - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback(). * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea2(). - * - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV2(). + * - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV3(). * - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback(). * - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback(). * - Possibly register a handler for C-Move SCP using OrthancPluginRegisterMoveCallback(). @@ -239,6 +239,8 @@ OrthancPluginErrorCode_SslInitialization = 39 /*!< Cannot initialize SSL encryption, check out your certificates */, OrthancPluginErrorCode_DiscontinuedAbi = 40 /*!< Calling a function that has been removed from the Orthanc Framework */, OrthancPluginErrorCode_BadRange = 41 /*!< Incorrect range request */, + OrthancPluginErrorCode_DatabaseCannotSerialize = 42 /*!< Database could not serialize access due to concurrent update, the transaction should be retried */, + OrthancPluginErrorCode_Revision = 43 /*!< A bad revision number was provided, which might indicate conflict between multiple writers */, OrthancPluginErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, OrthancPluginErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, @@ -518,12 +520,13 @@ _OrthancPluginService_GetInstanceDicomWebXml = 4019, /* New in Orthanc 1.7.0 */ /* Services for plugins implementing a database back-end */ - _OrthancPluginService_RegisterDatabaseBackend = 5000, + _OrthancPluginService_RegisterDatabaseBackend = 5000, /* New in Orthanc 0.8.6 */ _OrthancPluginService_DatabaseAnswer = 5001, - _OrthancPluginService_RegisterDatabaseBackendV2 = 5002, + _OrthancPluginService_RegisterDatabaseBackendV2 = 5002, /* New in Orthanc 0.9.4 */ _OrthancPluginService_StorageAreaCreate = 5003, _OrthancPluginService_StorageAreaRead = 5004, _OrthancPluginService_StorageAreaRemove = 5005, + _OrthancPluginService_RegisterDatabaseBackendV3 = 5006, /* New in Orthanc 1.9.2 */ /* Primitives for handling images */ _OrthancPluginService_GetImagePixelFormat = 6000, diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Resources/Configuration.json --- a/OrthancServer/Resources/Configuration.json Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Resources/Configuration.json Tue Apr 20 18:11:29 2021 +0200 @@ -768,5 +768,27 @@ // network protocol, expressed in bytes. This value affects both // Orthanc SCU and Orthanc SCP. It defaults to 16KB. The allowed // range is [4096,131072]. (new in Orthanc 1.9.0) - "MaximumPduLength" : 16384 + "MaximumPduLength" : 16384, + + // Arbitrary identifier of this Orthanc server when storing its + // global properties if a custom index plugin is used. This + // identifier is only useful in the case of multiple + // readers/writers, in order to avoid collisions between multiple + // Orthanc servers. If unset, this identifier is taken as a SHA-1 + // hash derived from the MAC adddresses of the network interfaces, + // and from the AET and TCP ports used by Orthanc. Manually setting + // this option is needed in Docker/Kubernetes environments. (new in + // Orthanc 1.9.2) + /** + "DatabaseServerIdentifier" : "Orthanc1", + **/ + + // Whether Orthanc protects the modification of metadata and + // attachments using revisions, which is done using the HTTP headers + // "ETag", "If-Match" and "If-None-Match" in the calls to the REST + // API. This is needed to handle collisions between concurrent + // modifications in the case of multiple writers. The database + // back-end must support this option, which is notably *not* yet the + // case of the built-in SQLite index. (new in Orthanc 1.9.2) + "CheckRevisions" : false } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Resources/RunCppCheck.sh --- a/OrthancServer/Resources/RunCppCheck.sh Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Tue Apr 20 18:11:29 2021 +0200 @@ -17,8 +17,8 @@ stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1194 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:164 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:72 -stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:385 -stlFindInsert:../../OrthancServer/Sources/ServerIndex.cpp:400 +stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:383 +stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:386 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:50 syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:130 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:321 diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp --- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -161,7 +161,7 @@ static void ApplyLevel(SetOfResources& candidates, - IDatabaseWrapper& database, + IDatabaseWrapper::ITransaction& transaction, ILookupResources& compatibility, const std::vector& lookup, ResourceType level) @@ -267,7 +267,7 @@ candidate != source.end(); ++candidate) { DicomMap tags; - database.GetMainDicomTags(tags, *candidate); + transaction.GetMainDicomTags(tags, *candidate); bool match = true; @@ -291,7 +291,7 @@ } - static std::string GetOneInstance(IDatabaseWrapper& compatibility, + static std::string GetOneInstance(IDatabaseWrapper::ITransaction& compatibility, int64_t resource, ResourceType level) { @@ -348,11 +348,11 @@ assert(upperLevel <= queryLevel && queryLevel <= lowerLevel); - SetOfResources candidates(database_, upperLevel); + SetOfResources candidates(transaction_, upperLevel); for (int level = upperLevel; level <= lowerLevel; level++) { - ApplyLevel(candidates, database_, compatibility_, lookup, static_cast(level)); + ApplyLevel(candidates, transaction_, compatibility_, lookup, static_cast(level)); if (level != lowerLevel) { @@ -372,7 +372,7 @@ it = resources.begin(); it != resources.end(); ++it) { int64_t parent; - if (database_.LookupParent(parent, *it)) + if (transaction_.LookupParent(parent, *it)) { parents.push_back(parent); } @@ -396,9 +396,9 @@ for (std::list::const_iterator it = resources.begin(); it != resources.end(); ++it, pos++) { - assert(database_.GetResourceType(*it) == queryLevel); + assert(transaction_.GetResourceType(*it) == queryLevel); - const std::string resource = database_.GetPublicId(*it); + const std::string resource = transaction_.GetPublicId(*it); resourcesId.push_back(resource); if (instancesId != NULL) @@ -411,7 +411,7 @@ else { // Collect one child instance for each of the selected resources - instancesId->push_back(GetOneInstance(database_, *it, queryLevel)); + instancesId->push_back(GetOneInstance(transaction_, *it, queryLevel)); } } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h --- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h Tue Apr 20 18:11:29 2021 +0200 @@ -43,13 +43,13 @@ class DatabaseLookup : public boost::noncopyable { private: - IDatabaseWrapper& database_; + IDatabaseWrapper::ITransaction& transaction_; ILookupResources& compatibility_; public: - DatabaseLookup(IDatabaseWrapper& database, + DatabaseLookup(IDatabaseWrapper::ITransaction& transaction, ILookupResources& compatibility) : - database_(database), + transaction_(transaction), compatibility_(compatibility) { } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp --- a/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -56,7 +56,8 @@ it = children.begin(); it != children.end(); ++it) { std::string value; - if (database.LookupMetadata(value, *it, metadata)) + int64_t revision; // Ignored + if (database.LookupMetadata(value, revision, *it, metadata)) { target.push_back(value); } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h --- a/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h Tue Apr 20 18:11:29 2021 +0200 @@ -49,6 +49,7 @@ int64_t id) = 0; virtual bool LookupMetadata(std::string& target, + int64_t& revision, int64_t id, MetadataType type) = 0; diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp --- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -41,7 +41,7 @@ namespace Compatibility { void ILookupResources::Apply( - IDatabaseWrapper& database, + IDatabaseWrapper::ITransaction& transaction, ILookupResources& compatibility, std::list& resourcesId, std::list* instancesId, @@ -49,7 +49,7 @@ ResourceType queryLevel, size_t limit) { - Compatibility::DatabaseLookup compat(database, compatibility); + Compatibility::DatabaseLookup compat(transaction, compatibility); compat.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit); } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/Compatibility/ILookupResources.h --- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.h Tue Apr 20 18:11:29 2021 +0200 @@ -66,7 +66,7 @@ const std::string& start, const std::string& end) = 0; - static void Apply(IDatabaseWrapper& database, + static void Apply(IDatabaseWrapper::ITransaction& transaction, ILookupResources& compatibility, std::list& resourcesId, std::list* instancesId, diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h --- a/OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h Tue Apr 20 18:11:29 2021 +0200 @@ -56,7 +56,8 @@ virtual void SetMetadata(int64_t id, MetadataType type, - const std::string& value) = 0; + const std::string& value, + int64_t revision) = 0; static void Apply(ISetResourcesContent& that, const ResourcesContent& content) diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp --- a/OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -90,7 +90,7 @@ it != resources_->end(); ++it) { std::list tmp; - database_.GetChildrenInternalId(tmp, *it); + transaction_.GetChildrenInternalId(tmp, *it); for (std::list::const_iterator child = tmp.begin(); child != tmp.end(); ++child) @@ -133,14 +133,14 @@ if (resources_.get() == NULL) { // All the resources of this level are part of the filter - database_.GetAllPublicIds(result, level_); + transaction_.GetAllPublicIds(result, level_); } else { for (Resources::const_iterator it = resources_->begin(); it != resources_->end(); ++it) { - result.push_back(database_.GetPublicId(*it)); + result.push_back(transaction_.GetPublicId(*it)); } } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/Compatibility/SetOfResources.h --- a/OrthancServer/Sources/Database/Compatibility/SetOfResources.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.h Tue Apr 20 18:11:29 2021 +0200 @@ -49,14 +49,14 @@ private: typedef std::set Resources; - IDatabaseWrapper& database_; + IDatabaseWrapper::ITransaction& transaction_; ResourceType level_; std::unique_ptr resources_; public: - SetOfResources(IDatabaseWrapper& database, + SetOfResources(IDatabaseWrapper::ITransaction& transaction, ResourceType level) : - database_(database), + transaction_(transaction), level_(level) { } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/IDatabaseListener.h --- a/OrthancServer/Sources/Database/IDatabaseListener.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseListener.h Tue Apr 20 18:11:29 2021 +0200 @@ -33,9 +33,10 @@ #pragma once +#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" #include "../ServerEnumerations.h" -#include "../ServerIndexChange.h" +#include #include namespace Orthanc @@ -50,8 +51,9 @@ virtual void SignalRemainingAncestor(ResourceType parentType, const std::string& publicId) = 0; - virtual void SignalFileDeleted(const FileInfo& info) = 0; + virtual void SignalAttachmentDeleted(const FileInfo& info) = 0; - virtual void SignalChange(const ServerIndexChange& change) = 0; + virtual void SignalResourceDeleted(ResourceType type, + const std::string& publicId) = 0; }; } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/IDatabaseWrapper.h --- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Apr 20 18:11:29 2021 +0200 @@ -36,9 +36,8 @@ #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" #include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" #include "../../../OrthancFramework/Sources/FileStorage/IStorageArea.h" -#include "../../../OrthancFramework/Sources/SQLite/ITransaction.h" - #include "../ExportedResource.h" +#include "../ServerIndexChange.h" #include "IDatabaseListener.h" #include @@ -54,21 +53,6 @@ class IDatabaseWrapper : public boost::noncopyable { public: - class ITransaction : public boost::noncopyable - { - public: - virtual ~ITransaction() - { - } - - virtual void Begin() = 0; - - virtual void Rollback() = 0; - - virtual void Commit(int64_t fileSizeDelta) = 0; - }; - - struct CreateInstanceResult { bool isNewPatient_; @@ -79,6 +63,192 @@ int64_t seriesId_; }; + + class ITransaction : public boost::noncopyable + { + public: + virtual ~ITransaction() + { + } + + 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; + + // A call to "AddAttachment()" guarantees that this attachment + // is not already existing. This is different from + // "SetMetadata()" that might have to replace an older value. + virtual void AddAttachment(int64_t id, + const FileInfo& attachment, + int64_t revision) = 0; + + virtual void ClearChanges() = 0; + + virtual void ClearExportedResources() = 0; + + virtual void DeleteAttachment(int64_t id, + FileContentType attachment) = 0; + + virtual void DeleteMetadata(int64_t id, + MetadataType type) = 0; + + virtual void DeleteResource(int64_t id) = 0; + + virtual void GetAllMetadata(std::map& target, + int64_t id) = 0; + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType) = 0; + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) = 0; + + virtual void GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) = 0; + + virtual void GetChildrenInternalId(std::list& target, + int64_t id) = 0; + + virtual void GetChildrenPublicId(std::list& target, + int64_t id) = 0; + + virtual void GetExportedResources(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) = 0; + + virtual void GetLastChange(std::list& target /*out*/) = 0; + + virtual void GetLastExportedResource(std::list& target /*out*/) = 0; + + virtual void GetMainDicomTags(DicomMap& map, + int64_t id) = 0; + + virtual std::string GetPublicId(int64_t resourceId) = 0; + + virtual uint64_t GetResourcesCount(ResourceType resourceType) = 0; + + virtual ResourceType GetResourceType(int64_t resourceId) = 0; + + virtual uint64_t GetTotalCompressedSize() = 0; + + virtual uint64_t GetTotalUncompressedSize() = 0; + + virtual bool IsExistingResource(int64_t internalId) = 0; + + virtual bool IsProtectedPatient(int64_t internalId) = 0; + + virtual void ListAvailableAttachments(std::set& target, + int64_t id) = 0; + + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change) = 0; + + virtual void LogExportedResource(const ExportedResource& resource) = 0; + + virtual bool LookupAttachment(FileInfo& attachment, + int64_t& revision, + int64_t id, + FileContentType contentType) = 0; + + /** + * If "shared" is "true", the property is shared by all the + * Orthanc servers that access the same database. If "shared" is + * "false", the property is private to the server (cf. the + * "DatabaseServerIdentifier" configuration option). + **/ + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property, + bool shared) = 0; + + virtual bool LookupMetadata(std::string& target, + int64_t& revision, + int64_t id, + MetadataType type) = 0; + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId) = 0; + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) = 0; + + virtual bool SelectPatientToRecycle(int64_t& internalId) = 0; + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) = 0; + + virtual void SetGlobalProperty(GlobalProperty property, + bool shared, + const std::string& value) = 0; + + virtual void ClearMainDicomTags(int64_t id) = 0; + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value, + int64_t revision) = 0; + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) = 0; + + + /** + * Primitives introduced in Orthanc 1.5.2 + **/ + + virtual bool IsDiskSizeAbove(uint64_t threshold) = 0; + + virtual void ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, // Can be NULL if not needed + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) = 0; + + // Returns "true" iff. the instance is new and has been inserted + // into the database. If "false" is returned, the content of + // "result" is undefined, but "instanceId" must be properly + // set. This method must also tag the parent patient as the most + // recent in the patient recycling order if it is not protected + // (so as to fix issue #58). + virtual bool CreateInstance(CreateInstanceResult& result, /* out */ + int64_t& instanceId, /* out */ + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance) = 0; + + // It is guaranteed that the resources to be modified have no main + // DICOM tags, and no DICOM identifiers associated with + // them. However, some metadata might be already existing, and + // have to be overwritten. + virtual void SetResourcesContent(const ResourcesContent& content) = 0; + + virtual void GetChildrenMetadata(std::list& target, + int64_t resourceId, + MetadataType metadata) = 0; + + virtual int64_t GetLastChangeIndex() = 0; + + + /** + * Primitives introduced in Orthanc 1.5.4 + **/ + + virtual bool LookupResourceAndParent(int64_t& id, + ResourceType& type, + std::string& parentPublicId, + const std::string& publicId) = 0; + }; + + virtual ~IDatabaseWrapper() { } @@ -87,171 +257,18 @@ virtual void Close() = 0; - virtual void AddAttachment(int64_t id, - const FileInfo& attachment) = 0; - - virtual void ClearChanges() = 0; - - virtual void ClearExportedResources() = 0; - - virtual void DeleteAttachment(int64_t id, - FileContentType attachment) = 0; - - virtual void DeleteMetadata(int64_t id, - MetadataType type) = 0; - - virtual void DeleteResource(int64_t id) = 0; - virtual void FlushToDisk() = 0; virtual bool HasFlushToDisk() const = 0; - virtual void GetAllMetadata(std::map& target, - int64_t id) = 0; - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType) = 0; - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) = 0; - - virtual void GetChanges(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) = 0; - - virtual void GetChildrenInternalId(std::list& target, - int64_t id) = 0; - - virtual void GetChildrenPublicId(std::list& target, - int64_t id) = 0; - - virtual void GetExportedResources(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) = 0; - - virtual void GetLastChange(std::list& target /*out*/) = 0; - - virtual void GetLastExportedResource(std::list& target /*out*/) = 0; - - virtual void GetMainDicomTags(DicomMap& map, - int64_t id) = 0; - - virtual std::string GetPublicId(int64_t resourceId) = 0; - - virtual uint64_t GetResourceCount(ResourceType resourceType) = 0; - - virtual ResourceType GetResourceType(int64_t resourceId) = 0; - - virtual uint64_t GetTotalCompressedSize() = 0; - - virtual uint64_t GetTotalUncompressedSize() = 0; - - virtual bool IsExistingResource(int64_t internalId) = 0; - - virtual bool IsProtectedPatient(int64_t internalId) = 0; - - virtual void ListAvailableAttachments(std::set& target, - int64_t id) = 0; - - virtual void LogChange(int64_t internalId, - const ServerIndexChange& change) = 0; - - virtual void LogExportedResource(const ExportedResource& resource) = 0; - - virtual bool LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType) = 0; - - virtual bool LookupGlobalProperty(std::string& target, - GlobalProperty property) = 0; - - virtual bool LookupMetadata(std::string& target, - int64_t id, - MetadataType type) = 0; - - virtual bool LookupParent(int64_t& parentId, - int64_t resourceId) = 0; - - virtual bool LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId) = 0; - - virtual bool SelectPatientToRecycle(int64_t& internalId) = 0; - - virtual bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid) = 0; - - virtual void SetGlobalProperty(GlobalProperty property, - const std::string& value) = 0; - - virtual void ClearMainDicomTags(int64_t id) = 0; - - virtual void SetMetadata(int64_t id, - MetadataType type, - const std::string& value) = 0; - - virtual void SetProtectedPatient(int64_t internalId, - bool isProtected) = 0; - - virtual ITransaction* StartTransaction() = 0; - - virtual void SetListener(IDatabaseListener& listener) = 0; + virtual ITransaction* StartTransaction(TransactionType type, + IDatabaseListener& listener) = 0; virtual unsigned int GetDatabaseVersion() = 0; virtual void Upgrade(unsigned int targetVersion, IStorageArea& storageArea) = 0; - - /** - * Primitives introduced in Orthanc 1.5.2 - **/ - - virtual bool IsDiskSizeAbove(uint64_t threshold) = 0; - - virtual void ApplyLookupResources(std::list& resourcesId, - std::list* instancesId, // Can be NULL if not needed - const std::vector& lookup, - ResourceType queryLevel, - size_t limit) = 0; - - // Returns "true" iff. the instance is new and has been inserted - // into the database. If "false" is returned, the content of - // "result" is undefined, but "instanceId" must be properly - // set. This method must also tag the parent patient as the most - // recent in the patient recycling order if it is not protected - // (so as to fix issue #58). - virtual bool CreateInstance(CreateInstanceResult& result, /* out */ - int64_t& instanceId, /* out */ - const std::string& patient, - const std::string& study, - const std::string& series, - const std::string& instance) = 0; - - // It is guaranteed that the resources to be modified have no main - // DICOM tags, and no DICOM identifiers associated with - // them. However, some metadata might be already existing, and - // have to be overwritten. - virtual void SetResourcesContent(const ResourcesContent& content) = 0; - - virtual void GetChildrenMetadata(std::list& target, - int64_t resourceId, - MetadataType metadata) = 0; - - virtual int64_t GetLastChangeIndex() = 0; - - - /** - * Primitives introduced in Orthanc 1.5.4 - **/ - - virtual bool LookupResourceAndParent(int64_t& id, - ResourceType& type, - std::string& parentPublicId, - const std::string& publicId) = 0; + virtual bool HasRevisionsSupport() const = 0; }; } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/ResourcesContent.cpp --- a/OrthancServer/Sources/Database/ResourcesContent.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/ResourcesContent.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -35,12 +35,119 @@ #include "ResourcesContent.h" #include "Compatibility/ISetResourcesContent.h" +#include "../ServerToolbox.h" + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" #include namespace Orthanc { + static void StoreMainDicomTagsInternal(ResourcesContent& target, + int64_t resource, + const DicomMap& tags) + { + DicomArray flattened(tags); + + for (size_t i = 0; i < flattened.GetSize(); i++) + { + const DicomElement& element = flattened.GetElement(i); + const DicomTag& tag = element.GetTag(); + const DicomValue& value = element.GetValue(); + if (!value.IsNull() && + !value.IsBinary()) + { + target.AddMainDicomTag(resource, tag, element.GetValue().GetContent()); + } + } + } + + + static void StoreIdentifiers(ResourcesContent& target, + int64_t resource, + ResourceType level, + const DicomMap& map) + { + const DicomTag* tags; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + // The identifiers tags are a subset of the main DICOM tags + assert(DicomMap::IsMainDicomTag(tags[i])); + + const DicomValue* value = map.TestAndGetValue(tags[i]); + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + std::string s = ServerToolbox::NormalizeIdentifier(value->GetContent()); + target.AddIdentifierTag(resource, tags[i], s); + } + } + } + + + void ResourcesContent::AddMetadata(int64_t resourceId, + MetadataType metadata, + const std::string& value) + { + if (isNewResource_) + { + metadata_.push_back(Metadata(resourceId, metadata, value)); + } + else + { + // This would require to handle the incrementation of revision + // numbers in the database backend => only allow setting + // metadata on new resources + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ResourcesContent::AddResource(int64_t resource, + ResourceType level, + const DicomMap& dicomSummary) + { + StoreIdentifiers(*this, resource, level, dicomSummary); + + DicomMap tags; + + switch (level) + { + case ResourceType_Patient: + dicomSummary.ExtractPatientInformation(tags); + break; + + case ResourceType_Study: + // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6) + dicomSummary.ExtractPatientInformation(tags); + StoreMainDicomTagsInternal(*this, resource, tags); + + dicomSummary.ExtractStudyInformation(tags); + break; + + case ResourceType_Series: + dicomSummary.ExtractSeriesInformation(tags); + break; + + case ResourceType_Instance: + dicomSummary.ExtractInstanceInformation(tags); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + StoreMainDicomTagsInternal(*this, resource, tags); + } + + void ResourcesContent::Store(Compatibility::ISetResourcesContent& compatibility) const { for (std::list::const_iterator @@ -59,7 +166,8 @@ for (std::list::const_iterator it = metadata_.begin(); it != metadata_.end(); ++it) { - compatibility.SetMetadata(it->resourceId_, it->metadata_, it->value_); + assert(isNewResource_); + compatibility.SetMetadata(it->resourceId_, it->metadata_, it->value_, 0 /* initial revision number */); } } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/ResourcesContent.h --- a/OrthancServer/Sources/Database/ResourcesContent.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/ResourcesContent.h Tue Apr 20 18:11:29 2021 +0200 @@ -89,10 +89,16 @@ typedef std::list ListMetadata; private: + bool isNewResource_; ListTags tags_; ListMetadata metadata_; public: + explicit ResourcesContent(bool isNewResource) : + isNewResource_(isNewResource) + { + } + void AddMainDicomTag(int64_t resourceId, const DicomTag& tag, const std::string& value) @@ -109,10 +115,7 @@ void AddMetadata(int64_t resourceId, MetadataType metadata, - const std::string& value) - { - metadata_.push_back(Metadata(resourceId, metadata, value)); - } + const std::string& value); void AddResource(int64_t resource, ResourceType level, diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -39,6 +39,11 @@ #include "../../../OrthancFramework/Sources/SQLite/Transaction.h" #include "../Search/ISqlLookupFormatter.h" #include "../ServerToolbox.h" +#include "Compatibility/ICreateInstance.h" +#include "Compatibility/IGetChildrenMetadata.h" +#include "Compatibility/ILookupResourceAndParent.h" +#include "Compatibility/ISetResourcesContent.h" +#include "VoidDatabaseListener.h" #include @@ -46,31 +51,1065 @@ #include namespace Orthanc -{ - namespace Internals +{ + class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter + { + private: + std::list values_; + + public: + virtual std::string GenerateParameter(const std::string& value) ORTHANC_OVERRIDE + { + values_.push_back(value); + return "?"; + } + + virtual std::string FormatResourceType(ResourceType level) ORTHANC_OVERRIDE + { + return boost::lexical_cast(level); + } + + virtual std::string FormatWildcardEscape() ORTHANC_OVERRIDE + { + return "ESCAPE '\\'"; + } + + void Bind(SQLite::Statement& statement) const + { + size_t pos = 0; + + for (std::list::const_iterator + it = values_.begin(); it != values_.end(); ++it, pos++) + { + statement.BindString(pos, *it); + } + } + }; + + + class SQLiteDatabaseWrapper::SignalRemainingAncestor : public SQLite::IScalarFunction + { + private: + bool hasRemainingAncestor_; + std::string remainingPublicId_; + ResourceType remainingType_; + + public: + SignalRemainingAncestor() : + hasRemainingAncestor_(false) + { + } + + void Reset() + { + hasRemainingAncestor_ = false; + } + + virtual const char* GetName() const ORTHANC_OVERRIDE + { + return "SignalRemainingAncestor"; + } + + virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE + { + return 2; + } + + virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE + { + CLOG(TRACE, SQLITE) << "There exists a remaining ancestor with public ID \"" + << context.GetStringValue(0) << "\" of type " + << context.GetIntValue(1); + + if (!hasRemainingAncestor_ || + remainingType_ >= context.GetIntValue(1)) + { + hasRemainingAncestor_ = true; + remainingPublicId_ = context.GetStringValue(0); + remainingType_ = static_cast(context.GetIntValue(1)); + } + } + + bool HasRemainingAncestor() const + { + return hasRemainingAncestor_; + } + + const std::string& GetRemainingAncestorId() const + { + assert(hasRemainingAncestor_); + return remainingPublicId_; + } + + ResourceType GetRemainingAncestorType() const + { + assert(hasRemainingAncestor_); + return remainingType_; + } + }; + + + class SQLiteDatabaseWrapper::TransactionBase : + public SQLiteDatabaseWrapper::UnitTestsTransaction, + public Compatibility::ICreateInstance, + public Compatibility::IGetChildrenMetadata, + public Compatibility::ILookupResourceAndParent, + public Compatibility::ISetResourcesContent { - class SignalFileDeleted : public SQLite::IScalarFunction + private: + void AnswerLookup(std::list& resourcesId, + std::list& instancesId, + ResourceType level) + { + resourcesId.clear(); + instancesId.clear(); + + std::unique_ptr statement; + + switch (level) + { + case ResourceType_Patient: + { + statement.reset( + new SQLite::Statement( + db_, SQLITE_FROM_HERE, + "SELECT patients.publicId, instances.publicID FROM Lookup AS patients " + "INNER JOIN Resources studies ON patients.internalId=studies.parentId " + "INNER JOIN Resources series ON studies.internalId=series.parentId " + "INNER JOIN Resources instances ON series.internalId=instances.parentId " + "GROUP BY patients.publicId")); + + break; + } + + case ResourceType_Study: + { + statement.reset( + new SQLite::Statement( + db_, SQLITE_FROM_HERE, + "SELECT studies.publicId, instances.publicID FROM Lookup AS studies " + "INNER JOIN Resources series ON studies.internalId=series.parentId " + "INNER JOIN Resources instances ON series.internalId=instances.parentId " + "GROUP BY studies.publicId")); + + break; + } + + case ResourceType_Series: + { + statement.reset( + new SQLite::Statement( + db_, SQLITE_FROM_HERE, + "SELECT series.publicId, instances.publicID FROM Lookup AS series " + "INNER JOIN Resources instances ON series.internalId=instances.parentId " + "GROUP BY series.publicId")); + + break; + } + + case ResourceType_Instance: + { + statement.reset( + new SQLite::Statement( + db_, SQLITE_FROM_HERE, "SELECT publicId, publicId FROM Lookup")); + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + assert(statement.get() != NULL); + + while (statement->Step()) + { + resourcesId.push_back(statement->ColumnString(0)); + instancesId.push_back(statement->ColumnString(1)); + } + } + + + void ClearTable(const std::string& tableName) + { + db_.Execute("DELETE FROM " + tableName); + } + + + void GetChangesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults) + { + target.clear(); + + while (target.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt64(0); + ChangeType changeType = static_cast(s.ColumnInt(1)); + ResourceType resourceType = static_cast(s.ColumnInt(3)); + const std::string& date = s.ColumnString(4); + + int64_t internalId = s.ColumnInt64(2); + std::string publicId = GetPublicId(internalId); + + target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date)); + } + + done = !(target.size() == maxResults && s.Step()); + } + + + void GetExportedResourcesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults) { - private: - IDatabaseListener& listener_; + target.clear(); + + while (target.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt64(0); + ResourceType resourceType = static_cast(s.ColumnInt(1)); + std::string publicId = s.ColumnString(2); + + ExportedResource resource(seq, + resourceType, + publicId, + s.ColumnString(3), // modality + s.ColumnString(8), // date + s.ColumnString(4), // patient ID + s.ColumnString(5), // study instance UID + s.ColumnString(6), // series instance UID + s.ColumnString(7)); // sop instance UID + + target.push_back(resource); + } + + done = !(target.size() == maxResults && s.Step()); + } + + + void GetChildren(std::list& childrenPublicIds, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?"); + s.BindInt64(0, id); + + childrenPublicIds.clear(); + while (s.Step()) + { + childrenPublicIds.push_back(s.ColumnString(0)); + } + } + + boost::mutex::scoped_lock lock_; + IDatabaseListener& listener_; + SignalRemainingAncestor& signalRemainingAncestor_; + + public: + TransactionBase(boost::mutex& mutex, + SQLite::Connection& db, + IDatabaseListener& listener, + SignalRemainingAncestor& signalRemainingAncestor) : + UnitTestsTransaction(db), + lock_(mutex), + listener_(listener), + signalRemainingAncestor_(signalRemainingAncestor) + { + } + + IDatabaseListener& GetListener() const + { + return listener_; + } + + + virtual void AddAttachment(int64_t id, + const FileInfo& attachment, + int64_t revision) ORTHANC_OVERRIDE + { + // TODO - REVISIONS + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, attachment.GetContentType()); + s.BindString(2, attachment.GetUuid()); + s.BindInt64(3, attachment.GetCompressedSize()); + s.BindInt64(4, attachment.GetUncompressedSize()); + s.BindInt(5, attachment.GetCompressionType()); + s.BindString(6, attachment.GetUncompressedMD5()); + s.BindString(7, attachment.GetCompressedMD5()); + s.Run(); + } + + + virtual void ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) ORTHANC_OVERRIDE + { + LookupFormatter formatter; + + std::string sql; + LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit); + + sql = "CREATE TEMPORARY TABLE Lookup AS " + sql; + + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup"); + s.Run(); + } + + { + SQLite::Statement statement(db_, sql); + formatter.Bind(statement); + statement.Run(); + } + + if (instancesId != NULL) + { + AnswerLookup(resourcesId, *instancesId, queryLevel); + } + else + { + resourcesId.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup"); + + while (s.Step()) + { + resourcesId.push_back(s.ColumnString(0)); + } + } + } + - public: - SignalFileDeleted(IDatabaseListener& listener) : - listener_(listener) + // From the "ICreateInstance" interface + virtual void AttachChild(int64_t parent, + int64_t child) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); + s.BindInt64(0, parent); + s.BindInt64(1, child); + s.Run(); + } + + + virtual void ClearChanges() ORTHANC_OVERRIDE + { + ClearTable("Changes"); + } + + virtual void ClearExportedResources() ORTHANC_OVERRIDE + { + ClearTable("ExportedResources"); + } + + + virtual void ClearMainDicomTags(int64_t id) ORTHANC_OVERRIDE + { + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?"); + s.BindInt64(0, id); + s.Run(); + } + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?"); + s.BindInt64(0, id); + s.Run(); + } + } + + + virtual bool CreateInstance(CreateInstanceResult& result, + int64_t& instanceId, + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance) ORTHANC_OVERRIDE + { + return ICreateInstance::Apply + (*this, result, instanceId, patient, study, series, instance); + } + + + // From the "ICreateInstance" interface + virtual int64_t CreateResource(const std::string& publicId, + ResourceType type) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)"); + s.BindInt(0, type); + s.BindString(1, publicId); + s.Run(); + return db_.GetLastInsertRowId(); + } + + + virtual void DeleteAttachment(int64_t id, + FileContentType attachment) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?"); + s.BindInt64(0, id); + s.BindInt(1, attachment); + s.Run(); + } + + + virtual void DeleteMetadata(int64_t id, + MetadataType type) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?"); + s.BindInt64(0, id); + s.BindInt(1, type); + s.Run(); + } + + + virtual void DeleteResource(int64_t id) ORTHANC_OVERRIDE + { + signalRemainingAncestor_.Reset(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); + s.BindInt64(0, id); + s.Run(); + + if (signalRemainingAncestor_.HasRemainingAncestor()) + { + listener_.SignalRemainingAncestor(signalRemainingAncestor_.GetRemainingAncestorType(), + signalRemainingAncestor_.GetRemainingAncestorId()); + } + } + + + virtual void GetAllMetadata(std::map& target, + int64_t id) ORTHANC_OVERRIDE + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + MetadataType key = static_cast(s.ColumnInt(0)); + target[key] = s.ColumnString(1); + } + } + + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + target.clear(); + while (s.Step()) + { + target.push_back(s.ColumnString(0)); + } + } + + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) ORTHANC_OVERRIDE + { + if (limit == 0) + { + target.clear(); + return; } - virtual const char* GetName() const + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT publicId FROM Resources WHERE " + "resourceType=? LIMIT ? OFFSET ?"); + s.BindInt(0, resourceType); + s.BindInt64(1, limit); + s.BindInt64(2, since); + + target.clear(); + while (s.Step()) + { + target.push_back(s.ColumnString(0)); + } + } + + + virtual void GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt64(0, since); + s.BindInt(1, maxResults + 1); + GetChangesInternal(target, done, s, maxResults); + } + + + virtual void GetChildrenMetadata(std::list& target, + int64_t resourceId, + MetadataType metadata) ORTHANC_OVERRIDE + { + IGetChildrenMetadata::Apply(*this, target, resourceId, metadata); + } + + + virtual void GetChildrenInternalId(std::list& target, + int64_t id) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b " + "WHERE a.parentId = b.internalId AND b.internalId = ?"); + s.BindInt64(0, id); + + target.clear(); + + while (s.Step()) + { + target.push_back(s.ColumnInt64(0)); + } + } + + + virtual void GetChildrenPublicId(std::list& target, + int64_t id) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " + "WHERE a.parentId = b.internalId AND b.internalId = ?"); + s.BindInt64(0, id); + + target.clear(); + + while (s.Step()) + { + target.push_back(s.ColumnString(0)); + } + } + + + virtual void GetExportedResources(std::list& target, + bool& done, + int64_t since, + uint32_t maxResults) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt64(0, since); + s.BindInt(1, maxResults + 1); + GetExportedResourcesInternal(target, done, s, maxResults); + } + + + virtual void GetLastChange(std::list& target /*out*/) ORTHANC_OVERRIDE + { + bool done; // Ignored + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); + GetChangesInternal(target, done, s, 1); + } + + + int64_t GetLastChangeIndex() ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT seq FROM sqlite_sequence WHERE name='Changes'"); + + if (s.Step()) + { + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + return c; + } + else + { + // No change has been recorded so far in the database + return 0; + } + } + + + virtual void GetLastExportedResource(std::list& target) ORTHANC_OVERRIDE + { + bool done; // Ignored + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1"); + GetExportedResourcesInternal(target, done, s, 1); + } + + + virtual void GetMainDicomTags(DicomMap& map, + int64_t id) ORTHANC_OVERRIDE + { + map.Clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); + s.BindInt64(0, id); + while (s.Step()) + { + map.SetValue(s.ColumnInt(1), + s.ColumnInt(2), + s.ColumnString(3), false); + } + } + + + virtual std::string GetPublicId(int64_t resourceId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT publicId FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (s.Step()) + { + return s.ColumnString(0); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + virtual uint64_t GetResourcesCount(ResourceType resourceType) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT COUNT(*) FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + if (!s.Step()) + { + return 0; + } + else + { + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + return c; + } + } + + + virtual ResourceType GetResourceType(int64_t resourceId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT resourceType FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (s.Step()) { - return "SignalFileDeleted"; + return static_cast(s.ColumnInt(0)); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE + { + // Old SQL query that was used in Orthanc <= 1.5.0: + // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0"); + s.Run(); + return static_cast(s.ColumnInt64(0)); + } + + + virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE + { + // Old SQL query that was used in Orthanc <= 1.5.0: + // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1"); + s.Run(); + return static_cast(s.ColumnInt64(0)); + } + + + virtual bool IsDiskSizeAbove(uint64_t threshold) ORTHANC_OVERRIDE + { + return GetTotalCompressedSize() > threshold; + } + + + virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM Resources WHERE internalId=?"); + s.BindInt64(0, internalId); + return s.Step(); + } + + + virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); + s.BindInt64(0, internalId); + return !s.Step(); + } + + + virtual void ListAvailableAttachments(std::set& target, + int64_t id) ORTHANC_OVERRIDE + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT fileType FROM AttachedFiles WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + target.insert(static_cast(s.ColumnInt(0))); + } + } + + + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); + s.BindInt(0, change.GetChangeType()); + s.BindInt64(1, internalId); + s.BindInt(2, change.GetResourceType()); + s.BindString(3, change.GetDate()); + s.Run(); + } + + + virtual void LogExportedResource(const ExportedResource& resource) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); + + s.BindInt(0, resource.GetResourceType()); + s.BindString(1, resource.GetPublicId()); + s.BindString(2, resource.GetModality()); + s.BindString(3, resource.GetPatientId()); + s.BindString(4, resource.GetStudyInstanceUid()); + s.BindString(5, resource.GetSeriesInstanceUid()); + s.BindString(6, resource.GetSopInstanceUid()); + s.BindString(7, resource.GetDate()); + s.Run(); + } + + + virtual bool LookupAttachment(FileInfo& attachment, + int64_t& revision, + int64_t id, + FileContentType contentType) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT uuid, uncompressedSize, compressionType, compressedSize, " + "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); + s.BindInt64(0, id); + s.BindInt(1, contentType); + + if (!s.Step()) + { + return false; + } + else + { + attachment = FileInfo(s.ColumnString(0), + contentType, + s.ColumnInt64(1), + s.ColumnString(4), + static_cast(s.ColumnInt(2)), + s.ColumnInt64(3), + s.ColumnString(5)); + revision = 0; // TODO - REVISIONS + return true; + } + } + + + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property, + bool shared) ORTHANC_OVERRIDE + { + // The "shared" info is not used by the SQLite database, as it + // can only be used by one Orthanc server. + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM GlobalProperties WHERE property=?"); + s.BindInt(0, property); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + return true; + } + } + + + virtual bool LookupMetadata(std::string& target, + int64_t& revision, + int64_t id, + MetadataType type) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM Metadata WHERE id=? AND type=?"); + s.BindInt64(0, id); + s.BindInt(1, type); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + revision = 0; // TODO - REVISIONS + return true; + } + } + + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT parentId FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_UnknownResource); } - virtual unsigned int GetCardinality() const + if (s.ColumnIsNull(0)) + { + return false; + } + else + { + parentId = s.ColumnInt(0); + return true; + } + } + + + virtual bool LookupResourceAndParent(int64_t& id, + ResourceType& type, + std::string& parentPublicId, + const std::string& publicId) ORTHANC_OVERRIDE + { + return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId); + } + + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); + s.BindString(0, publicId); + + if (!s.Step()) + { + return false; + } + else + { + id = s.ColumnInt(0); + type = static_cast(s.ColumnInt(1)); + + // Check whether there is a single resource with this public id + assert(!s.Step()); + + return true; + } + } + + + virtual bool SelectPatientToRecycle(int64_t& internalId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1"); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else + { + internalId = s.ColumnInt(0); + return true; + } + } + + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder " + "WHERE patientId != ? ORDER BY seq ASC LIMIT 1"); + s.BindInt64(0, patientIdToAvoid); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else { - return 7; + internalId = s.ColumnInt(0); + return true; + } + } + + + virtual void SetGlobalProperty(GlobalProperty property, + bool shared, + const std::string& value) ORTHANC_OVERRIDE + { + // The "shared" info is not used by the SQLite database, as it + // can only be used by one Orthanc server. + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); + s.BindInt(0, property); + s.BindString(1, value); + s.Run(); + } + + + // From the "ISetResourcesContent" interface + virtual void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, tag.GetGroup()); + s.BindInt(2, tag.GetElement()); + s.BindString(3, value); + s.Run(); + } + + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) ORTHANC_OVERRIDE + { + if (isProtected) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); + s.BindInt64(0, internalId); + s.Run(); + } + else if (IsProtectedPatient(internalId)) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); + s.BindInt64(0, internalId); + s.Run(); + } + else + { + // Nothing to do: The patient is already unprotected + } + } + + + // From the "ISetResourcesContent" interface + virtual void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, tag.GetGroup()); + s.BindInt(2, tag.GetElement()); + s.BindString(3, value); + s.Run(); + } + + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value, + int64_t revision) ORTHANC_OVERRIDE + { + // TODO - REVISIONS + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, type); + s.BindString(2, value); + s.Run(); + } + + + virtual void SetResourcesContent(const Orthanc::ResourcesContent& content) ORTHANC_OVERRIDE + { + ISetResourcesContent::Apply(*this, content); + } + + + // From the "ICreateInstance" interface + virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE + { + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); + s.BindInt64(0, patient); + s.Run(); + + assert(db_.GetLastChangeCount() == 0 || + db_.GetLastChangeCount() == 1); + + if (db_.GetLastChangeCount() == 0) + { + // The patient was protected, there was nothing to delete from the recycling order + return; + } } - virtual void Compute(SQLite::FunctionContext& context) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); + s.BindInt64(0, patient); + s.Run(); + } + } + }; + + + class SQLiteDatabaseWrapper::SignalFileDeleted : public SQLite::IScalarFunction + { + private: + SQLiteDatabaseWrapper& sqlite_; + + public: + SignalFileDeleted(SQLiteDatabaseWrapper& sqlite) : + sqlite_(sqlite) + { + } + + virtual const char* GetName() const ORTHANC_OVERRIDE + { + return "SignalFileDeleted"; + } + + virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE + { + return 7; + } + + virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE + { + if (sqlite_.activeTransaction_ != NULL) { std::string uncompressedMD5, compressedMD5; @@ -91,244 +1130,143 @@ static_cast(context.GetIntValue(3)), static_cast(context.GetInt64Value(4)), compressedMD5); - - listener_.SignalFileDeleted(info); - } - }; - - class SignalResourceDeleted : public SQLite::IScalarFunction - { - private: - IDatabaseListener& listener_; - - public: - SignalResourceDeleted(IDatabaseListener& listener) : - listener_(listener) - { - } - - virtual const char* GetName() const - { - return "SignalResourceDeleted"; - } - - virtual unsigned int GetCardinality() const - { - return 2; - } - virtual void Compute(SQLite::FunctionContext& context) - { - ResourceType type = static_cast(context.GetIntValue(1)); - ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0)); - listener_.SignalChange(change); + sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info); } - }; - - class SignalRemainingAncestor : public SQLite::IScalarFunction - { - private: - bool hasRemainingAncestor_; - std::string remainingPublicId_; - ResourceType remainingType_; + } + }; + - public: - SignalRemainingAncestor() : - hasRemainingAncestor_(false) - { - } - - void Reset() - { - hasRemainingAncestor_ = false; - } - - virtual const char* GetName() const - { - return "SignalRemainingAncestor"; - } + class SQLiteDatabaseWrapper::SignalResourceDeleted : public SQLite::IScalarFunction + { + private: + SQLiteDatabaseWrapper& sqlite_; - virtual unsigned int GetCardinality() const - { - return 2; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - CLOG(TRACE, SQLITE) << "There exists a remaining ancestor with public ID \"" - << context.GetStringValue(0) << "\" of type " - << context.GetIntValue(1); - - if (!hasRemainingAncestor_ || - remainingType_ >= context.GetIntValue(1)) - { - hasRemainingAncestor_ = true; - remainingPublicId_ = context.GetStringValue(0); - remainingType_ = static_cast(context.GetIntValue(1)); - } - } - - bool HasRemainingAncestor() const - { - return hasRemainingAncestor_; - } + public: + SignalResourceDeleted(SQLiteDatabaseWrapper& sqlite) : + sqlite_(sqlite) + { + } - const std::string& GetRemainingAncestorId() const - { - assert(hasRemainingAncestor_); - return remainingPublicId_; - } - - ResourceType GetRemainingAncestorType() const - { - assert(hasRemainingAncestor_); - return remainingType_; - } - }; - } - + virtual const char* GetName() const ORTHANC_OVERRIDE + { + return "SignalResourceDeleted"; + } - void SQLiteDatabaseWrapper::GetChangesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults) - { - target.clear(); - - while (target.size() < maxResults && s.Step()) + virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE { - int64_t seq = s.ColumnInt64(0); - ChangeType changeType = static_cast(s.ColumnInt(1)); - ResourceType resourceType = static_cast(s.ColumnInt(3)); - const std::string& date = s.ColumnString(4); - - int64_t internalId = s.ColumnInt64(2); - std::string publicId = GetPublicId(internalId); - - target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date)); + return 2; } - done = !(target.size() == maxResults && s.Step()); - } - - - void SQLiteDatabaseWrapper::GetExportedResourcesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults) - { - target.clear(); - - while (target.size() < maxResults && s.Step()) + virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE { - int64_t seq = s.ColumnInt64(0); - ResourceType resourceType = static_cast(s.ColumnInt(1)); - std::string publicId = s.ColumnString(2); + if (sqlite_.activeTransaction_ != NULL) + { + sqlite_.activeTransaction_->GetListener(). + SignalResourceDeleted(static_cast(context.GetIntValue(1)), + context.GetStringValue(0)); + } + } + }; + + + class SQLiteDatabaseWrapper::ReadWriteTransaction : public SQLiteDatabaseWrapper::TransactionBase + { + private: + SQLiteDatabaseWrapper& that_; + std::unique_ptr transaction_; + int64_t initialDiskSize_; - ExportedResource resource(seq, - resourceType, - publicId, - s.ColumnString(3), // modality - s.ColumnString(8), // date - s.ColumnString(4), // patient ID - s.ColumnString(5), // study instance UID - s.ColumnString(6), // series instance UID - s.ColumnString(7)); // sop instance UID + public: + ReadWriteTransaction(SQLiteDatabaseWrapper& that, + IDatabaseListener& listener) : + TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), + that_(that), + transaction_(new SQLite::Transaction(that_.db_)) + { + if (that_.activeTransaction_ != NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + that_.activeTransaction_ = this; - target.push_back(resource); +#if defined(NDEBUG) + // Release mode + initialDiskSize_ = 0; +#else + // Debug mode + initialDiskSize_ = static_cast(GetTotalCompressedSize()); +#endif } - done = !(target.size() == maxResults && s.Step()); - } + virtual ~ReadWriteTransaction() + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } + void Begin() + { + transaction_->Begin(); + } - void SQLiteDatabaseWrapper::GetChildren(std::list& childrenPublicIds, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?"); - s.BindInt64(0, id); + virtual void Rollback() ORTHANC_OVERRIDE + { + transaction_->Rollback(); + } - childrenPublicIds.clear(); - while (s.Step()) + virtual void Commit(int64_t fileSizeDelta /* only used in debug */) ORTHANC_OVERRIDE { - childrenPublicIds.push_back(s.ColumnString(0)); + transaction_->Commit(); + + assert(initialDiskSize_ + fileSizeDelta >= 0 && + initialDiskSize_ + fileSizeDelta == static_cast(GetTotalCompressedSize())); } - } + }; - void SQLiteDatabaseWrapper::DeleteResource(int64_t id) + class SQLiteDatabaseWrapper::ReadOnlyTransaction : public SQLiteDatabaseWrapper::TransactionBase { - signalRemainingAncestor_->Reset(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); - s.BindInt64(0, id); - s.Run(); - - if (signalRemainingAncestor_->HasRemainingAncestor() && - listener_ != NULL) - { - listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(), - signalRemainingAncestor_->GetRemainingAncestorId()); - } - } - - - bool SQLiteDatabaseWrapper::GetParentPublicId(std::string& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " - "WHERE a.internalId = b.parentId AND b.internalId = ?"); - s.BindInt64(0, id); - - if (s.Step()) + private: + SQLiteDatabaseWrapper& that_; + + public: + ReadOnlyTransaction(SQLiteDatabaseWrapper& that, + IDatabaseListener& listener) : + TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), + that_(that) { - target = s.ColumnString(0); - return true; + if (that_.activeTransaction_ != NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + that_.activeTransaction_ = this; } - else - { - return false; - } - } - - int64_t SQLiteDatabaseWrapper::GetTableRecordCount(const std::string& table) - { - /** - * "Generally one cannot use SQL parameters/placeholders for - * database identifiers (tables, columns, views, schemas, etc.) or - * database functions (e.g., CURRENT_DATE), but instead only for - * binding literal values." => To avoid any SQL injection, we - * check that the "table" parameter has only alphabetic - * characters. - * https://stackoverflow.com/a/1274764/881731 - **/ - for (size_t i = 0; i < table.size(); i++) + virtual ~ReadOnlyTransaction() + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } + + virtual void Rollback() ORTHANC_OVERRIDE { - if (!isalpha(table[i])) + } + + virtual void Commit(int64_t fileSizeDelta /* only used in debug */) ORTHANC_OVERRIDE + { + if (fileSizeDelta != 0) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_InternalError); } } - - // Don't use "SQLITE_FROM_HERE", otherwise "table" would be cached - SQLite::Statement s(db_, "SELECT COUNT(*) FROM " + table); + }; + - if (s.Step()) - { - int64_t c = s.ColumnInt(0); - assert(!s.Step()); - return c; - } - else - { - throw OrthancException(ErrorCode_InternalError); - } - } - - SQLiteDatabaseWrapper::SQLiteDatabaseWrapper(const std::string& path) : - listener_(NULL), + activeTransaction_(NULL), signalRemainingAncestor_(NULL), version_(0) { @@ -337,57 +1275,54 @@ SQLiteDatabaseWrapper::SQLiteDatabaseWrapper() : - listener_(NULL), + activeTransaction_(NULL), signalRemainingAncestor_(NULL), version_(0) { db_.OpenInMemory(); } - - int SQLiteDatabaseWrapper::GetGlobalIntegerProperty(GlobalProperty property, - int defaultValue) + SQLiteDatabaseWrapper::~SQLiteDatabaseWrapper() { - std::string tmp; - - if (!LookupGlobalProperty(tmp, GlobalProperty_DatabasePatchLevel)) + if (activeTransaction_ != NULL) { - return defaultValue; - } - else - { - try - { - return boost::lexical_cast(tmp); - } - catch (boost::bad_lexical_cast&) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Global property " + boost::lexical_cast(property) + - " should be an integer, but found: " + tmp); - } + LOG(ERROR) << "A SQLite transaction is still active in the SQLiteDatabaseWrapper destructor: Expect a crash"; } } void SQLiteDatabaseWrapper::Open() { - db_.Execute("PRAGMA ENCODING=\"UTF-8\";"); + { + boost::mutex::scoped_lock lock(mutex_); + + if (signalRemainingAncestor_ != NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // Cannot open twice + } + + signalRemainingAncestor_ = dynamic_cast(db_.Register(new SignalRemainingAncestor)); + db_.Register(new SignalFileDeleted(*this)); + db_.Register(new SignalResourceDeleted(*this)); + + db_.Execute("PRAGMA ENCODING=\"UTF-8\";"); - // Performance tuning of SQLite with PRAGMAs - // http://www.sqlite.org/pragma.html - db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;"); - db_.Execute("PRAGMA JOURNAL_MODE=WAL;"); - db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;"); - db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;"); - //db_.Execute("PRAGMA TEMP_STORE=memory"); + // Performance tuning of SQLite with PRAGMAs + // http://www.sqlite.org/pragma.html + db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;"); + db_.Execute("PRAGMA JOURNAL_MODE=WAL;"); + db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;"); + db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;"); + //db_.Execute("PRAGMA TEMP_STORE=memory"); - // Make "LIKE" case-sensitive in SQLite - db_.Execute("PRAGMA case_sensitive_like = true;"); - + // Make "LIKE" case-sensitive in SQLite + db_.Execute("PRAGMA case_sensitive_like = true;"); + } + + VoidDatabaseListener listener; + { - SQLite::Transaction t(db_); - t.Begin(); + std::unique_ptr transaction(StartTransaction(TransactionType_ReadOnly, listener)); if (!db_.DoesTableExist("GlobalProperties")) { @@ -399,7 +1334,7 @@ // Check the version of the database std::string tmp; - if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion)) + if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true /* unused in SQLite */)) { tmp = "Unknown"; } @@ -424,7 +1359,7 @@ // New in Orthanc 1.5.1 if (version_ == 6) { - if (!LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) || + if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true /* unused in SQLite */) || tmp != "1") { LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments"; @@ -434,14 +1369,18 @@ } } - t.Commit(); + transaction->Commit(0); } - - signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; - db_.Register(signalRemainingAncestor_); } + void SQLiteDatabaseWrapper::Close() + { + boost::mutex::scoped_lock lock(mutex_); + db_.Close(); + } + + static void ExecuteUpgradeScript(SQLite::Connection& db, ServerResources::FileResourceId script) { @@ -456,6 +1395,8 @@ void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion, IStorageArea& storageArea) { + boost::mutex::scoped_lock lock(mutex_); + if (targetVersion != 6) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); @@ -491,205 +1432,56 @@ // No change in the DB schema, the step from version 5 to 6 only // consists in reconstructing the main DICOM tags information // (as more tags got included). - db_.BeginTransaction(); - ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient); - ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study); - ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series); - ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance); - db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" + - boost::lexical_cast(GlobalProperty_DatabaseSchemaVersion) + ";"); - db_.CommitTransaction(); + + VoidDatabaseListener listener; + + { + std::unique_ptr transaction(StartTransaction(TransactionType_ReadWrite, listener)); + ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Patient); + ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Study); + ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Series); + ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Instance); + db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" + + boost::lexical_cast(GlobalProperty_DatabaseSchemaVersion) + ";"); + transaction->Commit(0); + } + version_ = 6; } } - void SQLiteDatabaseWrapper::SetListener(IDatabaseListener& listener) - { - listener_ = &listener; - db_.Register(new Internals::SignalFileDeleted(listener)); - db_.Register(new Internals::SignalResourceDeleted(listener)); - } - - - void SQLiteDatabaseWrapper::ClearTable(const std::string& tableName) - { - db_.Execute("DELETE FROM " + tableName); - } - - - bool SQLiteDatabaseWrapper::LookupParent(int64_t& parentId, - int64_t resourceId) + IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction(TransactionType type, + IDatabaseListener& listener) { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT parentId FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (!s.Step()) + switch (type) { - throw OrthancException(ErrorCode_UnknownResource); - } + case TransactionType_ReadOnly: + return new ReadOnlyTransaction(*this, listener); // This is a no-op transaction in SQLite (thanks to mutex) - if (s.ColumnIsNull(0)) - { - return false; - } - else - { - parentId = s.ColumnInt(0); - return true; + case TransactionType_ReadWrite: + { + std::unique_ptr transaction; + transaction.reset(new ReadWriteTransaction(*this, listener)); + transaction->Begin(); + return transaction.release(); + } + + default: + throw OrthancException(ErrorCode_InternalError); } } - - ResourceType SQLiteDatabaseWrapper::GetResourceType(int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT resourceType FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (s.Step()) - { - return static_cast(s.ColumnInt(0)); - } - else - { - throw OrthancException(ErrorCode_UnknownResource); - } - } - - - std::string SQLiteDatabaseWrapper::GetPublicId(int64_t resourceId) + + void SQLiteDatabaseWrapper::FlushToDisk() { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT publicId FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (s.Step()) - { - return s.ColumnString(0); - } - else - { - throw OrthancException(ErrorCode_UnknownResource); - } - } - - - void SQLiteDatabaseWrapper::GetChanges(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(0, since); - s.BindInt(1, maxResults + 1); - GetChangesInternal(target, done, s, maxResults); - } - - - void SQLiteDatabaseWrapper::GetLastChange(std::list& target /*out*/) - { - bool done; // Ignored - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); - GetChangesInternal(target, done, s, 1); + boost::mutex::scoped_lock lock(mutex_); + db_.FlushToDisk(); } - class SQLiteDatabaseWrapper::Transaction : public IDatabaseWrapper::ITransaction - { - private: - SQLiteDatabaseWrapper& that_; - std::unique_ptr transaction_; - int64_t initialDiskSize_; - - public: - Transaction(SQLiteDatabaseWrapper& that) : - that_(that), - transaction_(new SQLite::Transaction(that_.db_)) - { -#if defined(NDEBUG) - // Release mode - initialDiskSize_ = 0; -#else - // Debug mode - initialDiskSize_ = static_cast(that_.GetTotalCompressedSize()); -#endif - } - - virtual void Begin() - { - transaction_->Begin(); - } - - virtual void Rollback() - { - transaction_->Rollback(); - } - - virtual void Commit(int64_t fileSizeDelta /* only used in debug */) - { - transaction_->Commit(); - - assert(initialDiskSize_ + fileSizeDelta >= 0 && - initialDiskSize_ + fileSizeDelta == static_cast(that_.GetTotalCompressedSize())); - } - }; - - - IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction() - { - return new Transaction(*this); - } - - - void SQLiteDatabaseWrapper::GetAllMetadata(std::map& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - MetadataType key = static_cast(s.ColumnInt(0)); - target[key] = s.ColumnString(1); - } - } - - - void SQLiteDatabaseWrapper::SetGlobalProperty(GlobalProperty property, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); - s.BindInt(0, property); - s.BindString(1, value); - s.Run(); - } - - - bool SQLiteDatabaseWrapper::LookupGlobalProperty(std::string& target, - GlobalProperty property) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM GlobalProperties WHERE property=?"); - s.BindInt(0, property); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - - int64_t SQLiteDatabaseWrapper::CreateResource(const std::string& publicId, - ResourceType type) + int64_t SQLiteDatabaseWrapper::UnitTestsTransaction::CreateResource(const std::string& publicId, + ResourceType type) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)"); s.BindInt(0, type); @@ -699,33 +1491,8 @@ } - bool SQLiteDatabaseWrapper::LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); - s.BindString(0, publicId); - - if (!s.Step()) - { - return false; - } - else - { - id = s.ColumnInt(0); - type = static_cast(s.ColumnInt(1)); - - // Check whether there is a single resource with this public id - assert(!s.Step()); - - return true; - } - } - - - void SQLiteDatabaseWrapper::AttachChild(int64_t parent, - int64_t child) + void SQLiteDatabaseWrapper::UnitTestsTransaction::AttachChild(int64_t parent, + int64_t child) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); s.BindInt64(0, parent); @@ -734,150 +1501,9 @@ } - void SQLiteDatabaseWrapper::SetMetadata(int64_t id, - MetadataType type, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, type); - s.BindString(2, value); - s.Run(); - } - - - void SQLiteDatabaseWrapper::DeleteMetadata(int64_t id, - MetadataType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?"); - s.BindInt64(0, id); - s.BindInt(1, type); - s.Run(); - } - - - bool SQLiteDatabaseWrapper::LookupMetadata(std::string& target, - int64_t id, - MetadataType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM Metadata WHERE id=? AND type=?"); - s.BindInt64(0, id); - s.BindInt(1, type); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - - void SQLiteDatabaseWrapper::AddAttachment(int64_t id, - const FileInfo& attachment) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, attachment.GetContentType()); - s.BindString(2, attachment.GetUuid()); - s.BindInt64(3, attachment.GetCompressedSize()); - s.BindInt64(4, attachment.GetUncompressedSize()); - s.BindInt(5, attachment.GetCompressionType()); - s.BindString(6, attachment.GetUncompressedMD5()); - s.BindString(7, attachment.GetCompressedMD5()); - s.Run(); - } - - - void SQLiteDatabaseWrapper::DeleteAttachment(int64_t id, - FileContentType attachment) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?"); - s.BindInt64(0, id); - s.BindInt(1, attachment); - s.Run(); - } - - - void SQLiteDatabaseWrapper::ListAvailableAttachments(std::set& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT fileType FROM AttachedFiles WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - target.insert(static_cast(s.ColumnInt(0))); - } - } - - bool SQLiteDatabaseWrapper::LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT uuid, uncompressedSize, compressionType, compressedSize, " - "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); - s.BindInt64(0, id); - s.BindInt(1, contentType); - - if (!s.Step()) - { - return false; - } - else - { - attachment = FileInfo(s.ColumnString(0), - contentType, - s.ColumnInt64(1), - s.ColumnString(4), - static_cast(s.ColumnInt(2)), - s.ColumnInt64(3), - s.ColumnString(5)); - return true; - } - } - - - void SQLiteDatabaseWrapper::ClearMainDicomTags(int64_t id) - { - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?"); - s.BindInt64(0, id); - s.Run(); - } - - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?"); - s.BindInt64(0, id); - s.Run(); - } - } - - - void SQLiteDatabaseWrapper::SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, tag.GetGroup()); - s.BindInt(2, tag.GetElement()); - s.BindString(3, value); - s.Run(); - } - - - void SQLiteDatabaseWrapper::SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value) + void SQLiteDatabaseWrapper::UnitTestsTransaction::SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)"); s.BindInt64(0, id); @@ -888,427 +1514,40 @@ } - void SQLiteDatabaseWrapper::GetMainDicomTags(DicomMap& map, - int64_t id) + void SQLiteDatabaseWrapper::UnitTestsTransaction::SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) { - map.Clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); - s.BindInt64(0, id); - while (s.Step()) - { - map.SetValue(s.ColumnInt(1), - s.ColumnInt(2), - s.ColumnString(3), false); - } - } - - - void SQLiteDatabaseWrapper::GetChildrenPublicId(std::list& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " - "WHERE a.parentId = b.internalId AND b.internalId = ?"); + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); s.BindInt64(0, id); - - target.clear(); - - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - void SQLiteDatabaseWrapper::GetChildrenInternalId(std::list& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b " - "WHERE a.parentId = b.internalId AND b.internalId = ?"); - s.BindInt64(0, id); - - target.clear(); - - while (s.Step()) - { - target.push_back(s.ColumnInt64(0)); - } - } - - - void SQLiteDatabaseWrapper::LogChange(int64_t internalId, - const ServerIndexChange& change) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); - s.BindInt(0, change.GetChangeType()); - s.BindInt64(1, internalId); - s.BindInt(2, change.GetResourceType()); - s.BindString(3, change.GetDate()); + s.BindInt(1, tag.GetGroup()); + s.BindInt(2, tag.GetElement()); + s.BindString(3, value); s.Run(); } - void SQLiteDatabaseWrapper::LogExportedResource(const ExportedResource& resource) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); - - s.BindInt(0, resource.GetResourceType()); - s.BindString(1, resource.GetPublicId()); - s.BindString(2, resource.GetModality()); - s.BindString(3, resource.GetPatientId()); - s.BindString(4, resource.GetStudyInstanceUid()); - s.BindString(5, resource.GetSeriesInstanceUid()); - s.BindString(6, resource.GetSopInstanceUid()); - s.BindString(7, resource.GetDate()); - s.Run(); - } - - - void SQLiteDatabaseWrapper::GetExportedResources(std::list& target, - bool& done, - int64_t since, - uint32_t maxResults) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(0, since); - s.BindInt(1, maxResults + 1); - GetExportedResourcesInternal(target, done, s, maxResults); - } - - - void SQLiteDatabaseWrapper::GetLastExportedResource(std::list& target) - { - bool done; // Ignored - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1"); - GetExportedResourcesInternal(target, done, s, 1); - } - - - uint64_t SQLiteDatabaseWrapper::GetTotalCompressedSize() - { - // Old SQL query that was used in Orthanc <= 1.5.0: - // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0"); - s.Run(); - return static_cast(s.ColumnInt64(0)); - } - - - uint64_t SQLiteDatabaseWrapper::GetTotalUncompressedSize() + int64_t SQLiteDatabaseWrapper::UnitTestsTransaction::GetTableRecordCount(const std::string& table) { - // Old SQL query that was used in Orthanc <= 1.5.0: - // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1"); - s.Run(); - return static_cast(s.ColumnInt64(0)); - } - - - uint64_t SQLiteDatabaseWrapper::GetResourceCount(ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT COUNT(*) FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - if (!s.Step()) - { - return 0; - } - else + /** + * "Generally one cannot use SQL parameters/placeholders for + * database identifiers (tables, columns, views, schemas, etc.) or + * database functions (e.g., CURRENT_DATE), but instead only for + * binding literal values." => To avoid any SQL injection, we + * check that the "table" parameter has only alphabetic + * characters. + * https://stackoverflow.com/a/1274764/881731 + **/ + for (size_t i = 0; i < table.size(); i++) { - int64_t c = s.ColumnInt(0); - assert(!s.Step()); - return c; - } - } - - - void SQLiteDatabaseWrapper::GetAllPublicIds(std::list& target, - ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - void SQLiteDatabaseWrapper::GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) - { - if (limit == 0) - { - target.clear(); - return; + if (!isalpha(table[i])) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } } - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT publicId FROM Resources WHERE " - "resourceType=? LIMIT ? OFFSET ?"); - s.BindInt(0, resourceType); - s.BindInt64(1, limit); - s.BindInt64(2, since); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1"); - - if (!s.Step()) - { - // No patient remaining or all the patients are protected - return false; - } - else - { - internalId = s.ColumnInt(0); - return true; - } - } - - - bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT patientId FROM PatientRecyclingOrder " - "WHERE patientId != ? ORDER BY seq ASC LIMIT 1"); - s.BindInt64(0, patientIdToAvoid); - - if (!s.Step()) - { - // No patient remaining or all the patients are protected - return false; - } - else - { - internalId = s.ColumnInt(0); - return true; - } - } - - - bool SQLiteDatabaseWrapper::IsProtectedPatient(int64_t internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); - s.BindInt64(0, internalId); - return !s.Step(); - } - - - void SQLiteDatabaseWrapper::SetProtectedPatient(int64_t internalId, - bool isProtected) - { - if (isProtected) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); - s.BindInt64(0, internalId); - s.Run(); - } - else if (IsProtectedPatient(internalId)) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); - s.BindInt64(0, internalId); - s.Run(); - } - else - { - // Nothing to do: The patient is already unprotected - } - } - - - bool SQLiteDatabaseWrapper::IsExistingResource(int64_t internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM Resources WHERE internalId=?"); - s.BindInt64(0, internalId); - return s.Step(); - } - - - bool SQLiteDatabaseWrapper::IsDiskSizeAbove(uint64_t threshold) - { - return GetTotalCompressedSize() > threshold; - } - - - - class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter - { - private: - std::list values_; - - public: - virtual std::string GenerateParameter(const std::string& value) - { - values_.push_back(value); - return "?"; - } - - virtual std::string FormatResourceType(ResourceType level) - { - return boost::lexical_cast(level); - } - - virtual std::string FormatWildcardEscape() - { - return "ESCAPE '\\'"; - } - - void Bind(SQLite::Statement& statement) const - { - size_t pos = 0; - - for (std::list::const_iterator - it = values_.begin(); it != values_.end(); ++it, pos++) - { - statement.BindString(pos, *it); - } - } - }; - - - static void AnswerLookup(std::list& resourcesId, - std::list& instancesId, - SQLite::Connection& db, - ResourceType level) - { - resourcesId.clear(); - instancesId.clear(); - - std::unique_ptr statement; - - switch (level) - { - case ResourceType_Patient: - { - statement.reset( - new SQLite::Statement( - db, SQLITE_FROM_HERE, - "SELECT patients.publicId, instances.publicID FROM Lookup AS patients " - "INNER JOIN Resources studies ON patients.internalId=studies.parentId " - "INNER JOIN Resources series ON studies.internalId=series.parentId " - "INNER JOIN Resources instances ON series.internalId=instances.parentId " - "GROUP BY patients.publicId")); - - break; - } - - case ResourceType_Study: - { - statement.reset( - new SQLite::Statement( - db, SQLITE_FROM_HERE, - "SELECT studies.publicId, instances.publicID FROM Lookup AS studies " - "INNER JOIN Resources series ON studies.internalId=series.parentId " - "INNER JOIN Resources instances ON series.internalId=instances.parentId " - "GROUP BY studies.publicId")); - - break; - } - - case ResourceType_Series: - { - statement.reset( - new SQLite::Statement( - db, SQLITE_FROM_HERE, - "SELECT series.publicId, instances.publicID FROM Lookup AS series " - "INNER JOIN Resources instances ON series.internalId=instances.parentId " - "GROUP BY series.publicId")); - - break; - } - - case ResourceType_Instance: - { - statement.reset( - new SQLite::Statement( - db, SQLITE_FROM_HERE, "SELECT publicId, publicId FROM Lookup")); - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - assert(statement.get() != NULL); - - while (statement->Step()) - { - resourcesId.push_back(statement->ColumnString(0)); - instancesId.push_back(statement->ColumnString(1)); - } - } - - - void SQLiteDatabaseWrapper::ApplyLookupResources(std::list& resourcesId, - std::list* instancesId, - const std::vector& lookup, - ResourceType queryLevel, - size_t limit) - { - LookupFormatter formatter; - - std::string sql; - LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit); - - sql = "CREATE TEMPORARY TABLE Lookup AS " + sql; - - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup"); - s.Run(); - } - - { - SQLite::Statement statement(db_, sql); - formatter.Bind(statement); - statement.Run(); - } - - if (instancesId != NULL) - { - AnswerLookup(resourcesId, *instancesId, db_, queryLevel); - } - else - { - resourcesId.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup"); - - while (s.Step()) - { - resourcesId.push_back(s.ColumnString(0)); - } - } - } - - - int64_t SQLiteDatabaseWrapper::GetLastChangeIndex() - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT seq FROM sqlite_sequence WHERE name='Changes'"); + // Don't use "SQLITE_FROM_HERE", otherwise "table" would be cached + SQLite::Statement s(db_, "SELECT COUNT(*) FROM " + table); if (s.Step()) { @@ -1318,35 +1557,40 @@ } else { - // No change has been recorded so far in the database - return 0; + throw OrthancException(ErrorCode_InternalError); } } - void SQLiteDatabaseWrapper::TagMostRecentPatient(int64_t patient) + bool SQLiteDatabaseWrapper::UnitTestsTransaction::GetParentPublicId(std::string& target, + int64_t id) { - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); - s.BindInt64(0, patient); - s.Run(); + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " + "WHERE a.internalId = b.parentId AND b.internalId = ?"); + s.BindInt64(0, id); - assert(db_.GetLastChangeCount() == 0 || - db_.GetLastChangeCount() == 1); - - if (db_.GetLastChangeCount() == 0) - { - // The patient was protected, there was nothing to delete from the recycling order - return; - } + if (s.Step()) + { + target = s.ColumnString(0); + return true; + } + else + { + return false; } + } + + void SQLiteDatabaseWrapper::UnitTestsTransaction::GetChildren(std::list& childrenPublicIds, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?"); + s.BindInt64(0, id); + + childrenPublicIds.clear(); + while (s.Step()) { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); - s.BindInt64(0, patient); - s.Run(); + childrenPublicIds.push_back(s.ColumnString(0)); } } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Tue Apr 20 18:11:29 2021 +0200 @@ -36,38 +36,32 @@ #include "IDatabaseWrapper.h" #include "../../../OrthancFramework/Sources/SQLite/Connection.h" -#include "Compatibility/ICreateInstance.h" -#include "Compatibility/IGetChildrenMetadata.h" -#include "Compatibility/ILookupResourceAndParent.h" -#include "Compatibility/ISetResourcesContent.h" + +#include namespace Orthanc { - namespace Internals - { - class SignalRemainingAncestor; - } - /** * This class manages an instance of the Orthanc SQLite database. It * translates low-level requests into SQL statements. Mutual * exclusion MUST be implemented at a higher level. **/ - class SQLiteDatabaseWrapper : - public IDatabaseWrapper, - public Compatibility::ICreateInstance, - public Compatibility::IGetChildrenMetadata, - public Compatibility::ILookupResourceAndParent, - public Compatibility::ISetResourcesContent + class SQLiteDatabaseWrapper : public IDatabaseWrapper { private: - class Transaction; + class TransactionBase; + class SignalFileDeleted; + class SignalResourceDeleted; + class SignalRemainingAncestor; + class ReadOnlyTransaction; + class ReadWriteTransaction; class LookupFormatter; - IDatabaseListener* listener_; - SQLite::Connection db_; - Internals::SignalRemainingAncestor* signalRemainingAncestor_; - unsigned int version_; + boost::mutex mutex_; + SQLite::Connection db_; + TransactionBase* activeTransaction_; + SignalRemainingAncestor* signalRemainingAncestor_; + unsigned int version_; void GetChangesInternal(std::list& target, bool& done, @@ -79,294 +73,80 @@ SQLite::Statement& s, uint32_t maxResults); - void ClearTable(const std::string& tableName); - - // Unused => could be removed - int GetGlobalIntegerProperty(GlobalProperty property, - int defaultValue); - public: SQLiteDatabaseWrapper(const std::string& path); SQLiteDatabaseWrapper(); - virtual void Open() - ORTHANC_OVERRIDE; + virtual ~SQLiteDatabaseWrapper(); - virtual void Close() - ORTHANC_OVERRIDE - { - db_.Close(); - } + virtual void Open() ORTHANC_OVERRIDE; - virtual void SetListener(IDatabaseListener& listener) - ORTHANC_OVERRIDE; + virtual void Close() ORTHANC_OVERRIDE; - virtual bool LookupParent(int64_t& parentId, - int64_t resourceId) - ORTHANC_OVERRIDE; - - virtual std::string GetPublicId(int64_t resourceId) - ORTHANC_OVERRIDE; - - virtual ResourceType GetResourceType(int64_t resourceId) + virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type, + IDatabaseListener& listener) ORTHANC_OVERRIDE; - virtual void DeleteResource(int64_t id) - ORTHANC_OVERRIDE; - - virtual void GetChanges(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) - ORTHANC_OVERRIDE; - - virtual void GetLastChange(std::list& target /*out*/) - ORTHANC_OVERRIDE; + virtual void FlushToDisk() ORTHANC_OVERRIDE; - virtual IDatabaseWrapper::ITransaction* StartTransaction() - ORTHANC_OVERRIDE; - - virtual void FlushToDisk() - ORTHANC_OVERRIDE - { - db_.FlushToDisk(); - } - - virtual bool HasFlushToDisk() const - ORTHANC_OVERRIDE + virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE { return true; } - virtual void ClearChanges() - ORTHANC_OVERRIDE - { - ClearTable("Changes"); - } - - virtual void ClearExportedResources() - ORTHANC_OVERRIDE - { - ClearTable("ExportedResources"); - } - - virtual void GetAllMetadata(std::map& target, - int64_t id) - ORTHANC_OVERRIDE; - - virtual unsigned int GetDatabaseVersion() - ORTHANC_OVERRIDE + virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE { return version_; } virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) - ORTHANC_OVERRIDE; - + IStorageArea& storageArea) ORTHANC_OVERRIDE; - /** - * The methods declared below are for unit testing only! - **/ - - const char* GetErrorMessage() const + virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE { - return db_.GetErrorMessage(); + return false; // TODO - REVISIONS } - void GetChildren(std::list& childrenPublicIds, - int64_t id); - - int64_t GetTableRecordCount(const std::string& table); - - bool GetParentPublicId(std::string& target, - int64_t id); - - /** - * Until Orthanc 1.4.0, the methods below were part of the - * "DatabaseWrapperBase" class, that is now placed in the - * graveyard. + * The "StartTransaction()" method is guaranteed to return a class + * derived from "UnitTestsTransaction". The methods of + * "UnitTestsTransaction" give access to additional information + * about the underlying SQLite database to be used in unit tests. **/ - - virtual void SetGlobalProperty(GlobalProperty property, - const std::string& value) - ORTHANC_OVERRIDE; - - virtual bool LookupGlobalProperty(std::string& target, - GlobalProperty property) - ORTHANC_OVERRIDE; - - virtual int64_t CreateResource(const std::string& publicId, - ResourceType type) - ORTHANC_OVERRIDE; - - virtual bool LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId) - ORTHANC_OVERRIDE; - - virtual void AttachChild(int64_t parent, - int64_t child) - ORTHANC_OVERRIDE; - - virtual void SetMetadata(int64_t id, - MetadataType type, - const std::string& value) - ORTHANC_OVERRIDE; - - virtual void DeleteMetadata(int64_t id, - MetadataType type) - ORTHANC_OVERRIDE; - - virtual bool LookupMetadata(std::string& target, - int64_t id, - MetadataType type) - ORTHANC_OVERRIDE; - - virtual void AddAttachment(int64_t id, - const FileInfo& attachment) - ORTHANC_OVERRIDE; + class UnitTestsTransaction : public ITransaction + { + protected: + SQLite::Connection& db_; + + public: + UnitTestsTransaction(SQLite::Connection& db) : + db_(db) + { + } + + void GetChildren(std::list& childrenPublicIds, + int64_t id); - virtual void DeleteAttachment(int64_t id, - FileContentType attachment) - ORTHANC_OVERRIDE; - - virtual void ListAvailableAttachments(std::set& target, - int64_t id) - ORTHANC_OVERRIDE; - - virtual bool LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType) - ORTHANC_OVERRIDE; - - virtual void ClearMainDicomTags(int64_t id) - ORTHANC_OVERRIDE; - - virtual void SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value) - ORTHANC_OVERRIDE; - - virtual void SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value) - ORTHANC_OVERRIDE; - - virtual void GetMainDicomTags(DicomMap& map, - int64_t id) - ORTHANC_OVERRIDE; - - virtual void GetChildrenPublicId(std::list& target, - int64_t id) - ORTHANC_OVERRIDE; - - virtual void GetChildrenInternalId(std::list& target, - int64_t id) - ORTHANC_OVERRIDE; - - virtual void LogChange(int64_t internalId, - const ServerIndexChange& change) - ORTHANC_OVERRIDE; - - virtual void LogExportedResource(const ExportedResource& resource) - ORTHANC_OVERRIDE; + int64_t GetTableRecordCount(const std::string& table); - virtual void GetExportedResources(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) - ORTHANC_OVERRIDE; - - virtual void GetLastExportedResource(std::list& target /*out*/) - ORTHANC_OVERRIDE; - - virtual uint64_t GetTotalCompressedSize() - ORTHANC_OVERRIDE; - - virtual uint64_t GetTotalUncompressedSize() - ORTHANC_OVERRIDE; - - virtual uint64_t GetResourceCount(ResourceType resourceType) - ORTHANC_OVERRIDE; - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType) - ORTHANC_OVERRIDE; + bool GetParentPublicId(std::string& target, + int64_t id); - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) - ORTHANC_OVERRIDE; - - virtual bool SelectPatientToRecycle(int64_t& internalId) - ORTHANC_OVERRIDE; + int64_t CreateResource(const std::string& publicId, + ResourceType type); - virtual bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid) - ORTHANC_OVERRIDE; - - virtual bool IsProtectedPatient(int64_t internalId) - ORTHANC_OVERRIDE; - - virtual void SetProtectedPatient(int64_t internalId, - bool isProtected) - ORTHANC_OVERRIDE; - - virtual bool IsExistingResource(int64_t internalId) - ORTHANC_OVERRIDE; + void AttachChild(int64_t parent, + int64_t child); - virtual bool IsDiskSizeAbove(uint64_t threshold) - ORTHANC_OVERRIDE; - - virtual void ApplyLookupResources(std::list& resourcesId, - std::list* instancesId, - const std::vector& lookup, - ResourceType queryLevel, - size_t limit) - ORTHANC_OVERRIDE; - - virtual bool CreateInstance(CreateInstanceResult& result, - int64_t& instanceId, - const std::string& patient, - const std::string& study, - const std::string& series, - const std::string& instance) - ORTHANC_OVERRIDE - { - return ICreateInstance::Apply - (*this, result, instanceId, patient, study, series, instance); - } + void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value); - virtual void SetResourcesContent(const Orthanc::ResourcesContent& content) - ORTHANC_OVERRIDE - { - ISetResourcesContent::Apply(*this, content); - } - - virtual void GetChildrenMetadata(std::list& target, - int64_t resourceId, - MetadataType metadata) - ORTHANC_OVERRIDE - { - IGetChildrenMetadata::Apply(*this, target, resourceId, metadata); - } - - virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE; - - virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE; - - virtual bool LookupResourceAndParent(int64_t& id, - ResourceType& type, - std::string& parentPublicId, - const std::string& publicId) - ORTHANC_OVERRIDE - { - return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId); - } + void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value); + }; }; } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -0,0 +1,3377 @@ +/** + * 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) + { + transaction_.LogChange(internalId, change); + } + + GetTransactionContext().SignalChange(change); + } + + + SeriesStatus StatelessDatabaseOperations::ReadOnlyTransaction::GetSeriesStatus(int64_t id, + int64_t expectedNumberOfInstances) + { + std::list values; + transaction_.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, *context_)); + if (transaction_.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + ~Transaction() + { + if (!isCommitted_) + { + try + { + transaction_->Rollback(); + } + catch (OrthancException& e) + { + LOG(INFO) << "Cannot rollback transaction: " << e.What(); + } + } + } + + IDatabaseWrapper::ITransaction& GetDatabaseTransaction() + { + assert(transaction_.get() != NULL); + return *transaction_; + } + + 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) + { + boost::shared_lock lock(mutex_); // To protect "factory_" and "maxRetries_" + + 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 attempt = 0; + + for (;;) + { + try + { + 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 that was protecting the database. + **/ + + Transaction transaction(db_, *factory_, TransactionType_ReadOnly); // TODO - Only if not "TransactionType_Implicit" + { + ReadOnlyTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext()); + readOperations->Apply(t); + } + transaction.Commit(); + } + else + { + assert(writeOperations != NULL); + + Transaction transaction(db_, *factory_, TransactionType_ReadWrite); + { + ReadWriteTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext()); + writeOperations->Apply(t); + } + transaction.Commit(); + } + + return; // Success + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_DatabaseCannotSerialize) + { + if (attempt >= maxRetries_) + { + throw; + } + else + { + attempt++; + + // The "rand()" adds some jitter to de-synchronize writers + boost::this_thread::sleep(boost::posix_time::milliseconds(100 * attempt + 5 * (rand() % 10))); + } + } + else + { + throw; + } + } + } + } + + + StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : + db_(db), + mainDicomTagsRegistry_(new MainDicomTagsRegistry), + hasFlushToDisk_(db.HasFlushToDisk()), + maxRetries_(0) + { + } + + + void StatelessDatabaseOperations::FlushToDisk() + { + try + { + db_.FlushToDisk(); + } + catch (OrthancException&) + { + LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)"; + } + } + + + void StatelessDatabaseOperations::SetTransactionContextFactory(ITransactionContextFactory* factory) + { + boost::unique_lock lock(mutex_); + + if (factory == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (factory_.get() != NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + factory_.reset(factory); + } + } + + + void StatelessDatabaseOperations::SetMaxDatabaseRetries(unsigned int maxRetries) + { + boost::unique_lock lock(mutex_); + maxRetries_ = maxRetries; + } + + + 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; + int64_t revision; // ignored + if (!transaction.LookupAttachment(attachment, revision, 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, + int64_t& revision, + const std::string& instancePublicId, + FileContentType contentType) + { + class Operations : public ReadOnlyOperationsT5 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + int64_t internalId; + ResourceType type; + if (!transaction.LookupResource(internalId, type, tuple.get<3>())) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else if (transaction.LookupAttachment(tuple.get<1>(), tuple.get<2>(), internalId, tuple.get<4>())) + { + assert(tuple.get<1>().GetContentType() == tuple.get<4>()); + tuple.get<0>() = true; + } + else + { + tuple.get<0>() = false; + } + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, attachment, revision, 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.GetResourcesCount(ResourceType_Patient); + tuple.get<3>() = transaction.GetResourcesCount(ResourceType_Study); + tuple.get<4>() = transaction.GetResourcesCount(ResourceType_Series); + tuple.get<5>() = transaction.GetResourcesCount(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, + int64_t& revision, + const std::string& publicId, + ResourceType expectedType, + MetadataType type) + { + class Operations : public ReadOnlyOperationsT6 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + ResourceType resourceType; + int64_t id; + if (!transaction.LookupResource(id, resourceType, tuple.get<3>()) || + resourceType != tuple.get<4>()) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + tuple.get<0>() = transaction.LookupMetadata(tuple.get<1>(), tuple.get<2>(), id, tuple.get<5>()); + } + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, target, revision, 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; + int64_t revision; // ignored + if (transaction.LookupAttachment(attachment, revision, 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, + bool shared) + { + class Operations : public ReadOnlyOperationsT4 + { + 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>(), tuple.get<3>()); + } + }; + + bool found; + Operations operations; + operations.Apply(*this, found, value, property, shared); + return found; + } + + + std::string StatelessDatabaseOperations::GetGlobalProperty(GlobalProperty property, + bool shared, + const std::string& defaultValue) + { + std::string s; + if (LookupGlobalProperty(s, property, shared)) + { + 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(int64_t& newRevision, + const std::string& publicId, + MetadataType type, + const std::string& value, + bool hasOldRevision, + int64_t oldRevision) + { + class Operations : public IReadWriteOperations + { + private: + int64_t& newRevision_; + const std::string& publicId_; + MetadataType type_; + const std::string& value_; + bool hasOldRevision_; + int64_t oldRevision_; + + public: + Operations(int64_t& newRevision, + const std::string& publicId, + MetadataType type, + const std::string& value, + bool hasOldRevision, + int64_t oldRevision) : + newRevision_(newRevision), + publicId_(publicId), + type_(type), + value_(value), + hasOldRevision_(hasOldRevision), + oldRevision_(oldRevision) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + ResourceType resourceType; + int64_t id; + if (!transaction.LookupResource(id, resourceType, publicId_)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + std::string oldValue; + int64_t expectedRevision; + if (transaction.LookupMetadata(oldValue, expectedRevision, id, type_)) + { + if (hasOldRevision_ && + expectedRevision != oldRevision_) + { + throw OrthancException(ErrorCode_Revision); + } + else + { + newRevision_ = expectedRevision + 1; + } + } + else + { + // The metadata is not existing yet: Ignore "oldRevision" + // and initialize a new sequence of revisions + newRevision_ = 0; + } + + transaction.SetMetadata(id, type_, value_, newRevision_); + + if (IsUserMetadata(type_)) + { + transaction.LogChange(id, ChangeType_UpdatedMetadata, resourceType, publicId_); + } + } + } + }; + + Operations operations(newRevision, publicId, type, value, hasOldRevision, oldRevision); + Apply(operations); + } + + + void StatelessDatabaseOperations::OverwriteMetadata(const std::string& publicId, + MetadataType type, + const std::string& value) + { + int64_t newRevision; // Unused + SetMetadata(newRevision, publicId, type, value, false /* no old revision */, -1 /* dummy */); + } + + + bool StatelessDatabaseOperations::DeleteMetadata(const std::string& publicId, + MetadataType type, + bool hasRevision, + int64_t revision) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& publicId_; + MetadataType type_; + bool hasRevision_; + int64_t revision_; + bool found_; + + public: + Operations(const std::string& publicId, + MetadataType type, + bool hasRevision, + int64_t revision) : + publicId_(publicId), + type_(type), + hasRevision_(hasRevision), + revision_(revision), + found_(false) + { + } + + bool HasFound() const + { + return found_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + ResourceType resourceType; + int64_t id; + if (!transaction.LookupResource(id, resourceType, publicId_)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + std::string s; + int64_t expectedRevision; + if (transaction.LookupMetadata(s, expectedRevision, id, type_)) + { + if (hasRevision_ && + expectedRevision != revision_) + { + throw OrthancException(ErrorCode_Revision); + } + + found_ = true; + transaction.DeleteMetadata(id, type_); + + if (IsUserMetadata(type_)) + { + transaction.LogChange(id, ChangeType_UpdatedMetadata, resourceType, publicId_); + } + } + else + { + found_ = false; + } + } + } + }; + + Operations operations(publicId, type, hasRevision, revision); + Apply(operations); + return operations.HasFound(); + } + + + uint64_t StatelessDatabaseOperations::IncrementGlobalSequence(GlobalProperty sequence, + bool shared) + { + class Operations : public IReadWriteOperations + { + private: + uint64_t newValue_; + GlobalProperty sequence_; + bool shared_; + + public: + Operations(GlobalProperty sequence, + bool shared) : + newValue_(0), // Dummy initialization + sequence_(sequence), + shared_(shared) + { + } + + uint64_t GetNewValue() const + { + return newValue_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + std::string oldString; + + if (transaction.LookupGlobalProperty(oldString, sequence_, shared_)) + { + 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_, shared_, boost::lexical_cast(newValue_)); + } + }; + + Operations operations(sequence, shared); + 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, + bool shared, + const std::string& value) + { + class Operations : public IReadWriteOperations + { + private: + GlobalProperty property_; + bool shared_; + const std::string& value_; + + public: + Operations(GlobalProperty property, + bool shared, + const std::string& value) : + property_(property), + shared_(shared), + value_(value) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.SetGlobalProperty(property_, shared_, value_); + } + }; + + Operations operations(property, shared, value); + Apply(operations); + } + + + bool StatelessDatabaseOperations::DeleteAttachment(const std::string& publicId, + FileContentType type, + bool hasRevision, + int64_t revision) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& publicId_; + FileContentType type_; + bool hasRevision_; + int64_t revision_; + bool found_; + + public: + Operations(const std::string& publicId, + FileContentType type, + bool hasRevision, + int64_t revision) : + publicId_(publicId), + type_(type), + hasRevision_(hasRevision), + revision_(revision), + found_(false) + { + } + + bool HasFound() const + { + return found_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + ResourceType resourceType; + int64_t id; + if (!transaction.LookupResource(id, resourceType, publicId_)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + FileInfo info; + int64_t expectedRevision; + if (transaction.LookupAttachment(info, expectedRevision, id, type_)) + { + if (hasRevision_ && + expectedRevision != revision_) + { + throw OrthancException(ErrorCode_Revision); + } + + found_ = true; + transaction.DeleteAttachment(id, type_); + + if (IsUserContentType(type_)) + { + transaction.LogChange(id, ChangeType_UpdatedAttachment, resourceType, publicId_); + } + } + else + { + found_ = false; + } + } + } + }; + + Operations operations(publicId, type, hasRevision, revision); + Apply(operations); + return operations.HasFound(); + } + + + void StatelessDatabaseOperations::LogChange(int64_t internalId, + ChangeType changeType, + const std::string& publicId, + ResourceType level) + { + class Operations : public IReadWriteOperations + { + private: + int64_t internalId_; + ChangeType changeType_; + const std::string& publicId_; + ResourceType level_; + + public: + Operations(int64_t internalId, + ChangeType changeType, + const std::string& publicId, + ResourceType level) : + internalId_(internalId), + changeType_(changeType), + publicId_(publicId), + level_(level) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + int64_t id; + ResourceType type; + if (transaction.LookupResource(id, type, publicId_) && + id == internalId_) + { + /** + * Make sure that the resource is still existing, with the + * same internal ID, which indicates the absence of bouncing + * (if deleting then recreating the same resource). Don't + * throw an exception if the resource has been deleted, + * because this function might e.g. be called from + * "StatelessDatabaseOperations::UnstableResourcesMonitorThread()" + * (for which a deleted resource is *not* an error case). + **/ + if (type == level_) + { + transaction.LogChange(id, changeType_, type, publicId_); + } + else + { + // Consistency check + throw OrthancException(ErrorCode_UnknownResource); + } + } + } + }; + + Operations operations(internalId, 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_; + + static void ReplaceMetadata(ReadWriteTransaction& transaction, + int64_t instance, + MetadataType metadata, + const std::string& value) + { + std::string oldValue; + int64_t oldRevision; + + if (transaction.LookupMetadata(oldValue, oldRevision, instance, metadata)) + { + transaction.SetMetadata(instance, metadata, value, oldRevision + 1); + } + else + { + transaction.SetMetadata(instance, metadata, value, 0); + } + } + + public: + explicit 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(false /* prevent the setting of metadata */); + 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_) + { + ReplaceMetadata(transaction, instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_)); + } + + const DicomValue* value; + if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && + !value->IsNull() && + !value->IsBinary()) + { + ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent()); + } + } + }; + + Operations operations(dicom); + Apply(operations); + } + + + static bool IsRecyclingNeeded(IDatabaseWrapper::ITransaction& transaction, + 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 (transaction.IsDiskSizeAbove(maximumStorageSize - addedInstanceSize)) + { + return true; + } + } + + if (maximumPatients != 0) + { + uint64_t patientCount = transaction.GetResourcesCount(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(transaction_, 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 = transaction_.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 ? + transaction_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) : + transaction_.SelectPatientToRecycle(patientToRecycle)); + + if (!ok) + { + throw OrthancException(ErrorCode_FullStorage); + } + + LOG(TRACE) << "Recycling one patient"; + transaction_.DeleteResource(patientToRecycle); + + if (!IsRecyclingNeeded(transaction_, 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, ""); + } + }; + + if (maximumStorageSize != 0 || + 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, 0 /* this is the first revision */); + } + + + { + ResourcesContent content(true /* new resource, metadata can be set */); + + // 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) + { + if (e.GetErrorCode() == ErrorCode_DatabaseCannotSerialize) + { + throw; + } + else + { + 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(int64_t& newRevision, + const FileInfo& attachment, + const std::string& publicId, + uint64_t maximumStorageSize, + unsigned int maximumPatients, + bool hasOldRevision, + int64_t oldRevision) + { + class Operations : public IReadWriteOperations + { + private: + int64_t& newRevision_; + StoreStatus status_; + const FileInfo& attachment_; + const std::string& publicId_; + uint64_t maximumStorageSize_; + unsigned int maximumPatientCount_; + bool hasOldRevision_; + int64_t oldRevision_; + + public: + Operations(int64_t& newRevision, + const FileInfo& attachment, + const std::string& publicId, + uint64_t maximumStorageSize, + unsigned int maximumPatientCount, + bool hasOldRevision, + int64_t oldRevision) : + newRevision_(newRevision), + status_(StoreStatus_Failure), + attachment_(attachment), + publicId_(publicId), + maximumStorageSize_(maximumStorageSize), + maximumPatientCount_(maximumPatientCount), + hasOldRevision_(hasOldRevision), + oldRevision_(oldRevision) + { + } + + 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 + { + // Possibly remove previous attachment + { + FileInfo oldFile; + int64_t expectedRevision; + if (transaction.LookupAttachment(oldFile, expectedRevision, resourceId, attachment_.GetContentType())) + { + if (hasOldRevision_ && + expectedRevision != oldRevision_) + { + throw OrthancException(ErrorCode_Revision); + } + else + { + newRevision_ = expectedRevision + 1; + transaction.DeleteAttachment(resourceId, attachment_.GetContentType()); + } + } + else + { + // The attachment is not existing yet: Ignore "oldRevision" + // and initialize a new sequence of revisions + newRevision_ = 0; + } + } + + // 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_, newRevision_); + + if (IsUserContentType(attachment_.GetContentType())) + { + transaction.LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId_); + } + + transaction.GetTransactionContext().SignalAttachmentsAdded(attachment_.GetCompressedSize()); + + status_ = StoreStatus_Success; + } + } + }; + + + Operations operations(newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision); + Apply(operations); + return operations.GetStatus(); + } +} diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/StatelessDatabaseOperations.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Apr 20 18:11:29 2021 +0200 @@ -0,0 +1,639 @@ +/** + * 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 +#include + + +namespace Orthanc +{ + class DatabaseLookup; + class ParsedDicomFile; + struct ServerIndexChange; + + class StatelessDatabaseOperations : public boost::noncopyable + { + public: + typedef std::list Attachments; + typedef std::map, std::string> MetadataMap; + + class ITransactionContext : public IDatabaseListener + { + 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() + { + } + + // WARNING: This method can be invoked from several threads concurrently + virtual ITransactionContext* Create() = 0; + }; + + + class ReadOnlyTransaction : public boost::noncopyable + { + private: + ITransactionContext& context_; + + protected: + IDatabaseWrapper::ITransaction& transaction_; + + public: + explicit ReadOnlyTransaction(IDatabaseWrapper::ITransaction& transaction, + ITransactionContext& context) : + context_(context), + transaction_(transaction) + { + } + + 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 transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit); + } + + void GetAllMetadata(std::map& target, + int64_t id) + { + transaction_.GetAllMetadata(target, id); + } + + void GetAllPublicIds(std::list& target, + ResourceType resourceType) + { + return transaction_.GetAllPublicIds(target, resourceType); + } + + void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) + { + return transaction_.GetAllPublicIds(target, resourceType, since, limit); + } + + void GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + { + transaction_.GetChanges(target, done, since, maxResults); + } + + void GetChildrenInternalId(std::list& target, + int64_t id) + { + transaction_.GetChildrenInternalId(target, id); + } + + void GetChildrenPublicId(std::list& target, + int64_t id) + { + transaction_.GetChildrenPublicId(target, id); + } + + void GetExportedResources(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + { + return transaction_.GetExportedResources(target, done, since, maxResults); + } + + void GetLastChange(std::list& target /*out*/) + { + transaction_.GetLastChange(target); + } + + void GetLastExportedResource(std::list& target /*out*/) + { + return transaction_.GetLastExportedResource(target); + } + + int64_t GetLastChangeIndex() + { + return transaction_.GetLastChangeIndex(); + } + + void GetMainDicomTags(DicomMap& map, + int64_t id) + { + transaction_.GetMainDicomTags(map, id); + } + + std::string GetPublicId(int64_t resourceId) + { + return transaction_.GetPublicId(resourceId); + } + + uint64_t GetResourcesCount(ResourceType resourceType) + { + return transaction_.GetResourcesCount(resourceType); + } + + ResourceType GetResourceType(int64_t resourceId) + { + return transaction_.GetResourceType(resourceId); + } + + uint64_t GetTotalCompressedSize() + { + return transaction_.GetTotalCompressedSize(); + } + + uint64_t GetTotalUncompressedSize() + { + return transaction_.GetTotalUncompressedSize(); + } + + bool IsProtectedPatient(int64_t internalId) + { + return transaction_.IsProtectedPatient(internalId); + } + + void ListAvailableAttachments(std::set& target, + int64_t id) + { + transaction_.ListAvailableAttachments(target, id); + } + + bool LookupAttachment(FileInfo& attachment, + int64_t& revision, + int64_t id, + FileContentType contentType) + { + return transaction_.LookupAttachment(attachment, revision, id, contentType); + } + + bool LookupGlobalProperty(std::string& target, + GlobalProperty property, + bool shared) + { + return transaction_.LookupGlobalProperty(target, property, shared); + } + + bool LookupMetadata(std::string& target, + int64_t& revision, + int64_t id, + MetadataType type) + { + return transaction_.LookupMetadata(target, revision, id, type); + } + + bool LookupParent(int64_t& parentId, + int64_t resourceId) + { + return transaction_.LookupParent(parentId, resourceId); + } + + bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) + { + return transaction_.LookupResource(id, type, publicId); + } + + bool LookupResourceAndParent(int64_t& id, + ResourceType& type, + std::string& parentPublicId, + const std::string& publicId) + { + return transaction_.LookupResourceAndParent(id, type, parentPublicId, publicId); + } + }; + + + class ReadWriteTransaction : public ReadOnlyTransaction + { + public: + ReadWriteTransaction(IDatabaseWrapper::ITransaction& transaction, + ITransactionContext& context) : + ReadOnlyTransaction(transaction, context) + { + } + + void AddAttachment(int64_t id, + const FileInfo& attachment, + int64_t revision) + { + transaction_.AddAttachment(id, attachment, revision); + } + + void ClearChanges() + { + transaction_.ClearChanges(); + } + + void ClearExportedResources() + { + transaction_.ClearExportedResources(); + } + + void ClearMainDicomTags(int64_t id) + { + return transaction_.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 transaction_.CreateInstance(result, instanceId, patient, study, series, instance); + } + + void DeleteAttachment(int64_t id, + FileContentType attachment) + { + return transaction_.DeleteAttachment(id, attachment); + } + + void DeleteMetadata(int64_t id, + MetadataType type) + { + transaction_.DeleteMetadata(id, type); + } + + void DeleteResource(int64_t id) + { + transaction_.DeleteResource(id); + } + + void LogChange(int64_t internalId, + ChangeType changeType, + ResourceType resourceType, + const std::string& publicId); + + void LogExportedResource(const ExportedResource& resource) + { + transaction_.LogExportedResource(resource); + } + + void SetGlobalProperty(GlobalProperty property, + bool shared, + const std::string& value) + { + transaction_.SetGlobalProperty(property, shared, value); + } + + void SetMetadata(int64_t id, + MetadataType type, + const std::string& value, + int64_t revision) + { + return transaction_.SetMetadata(id, type, value, revision); + } + + void SetProtectedPatient(int64_t internalId, + bool isProtected) + { + transaction_.SetProtectedPatient(internalId, isProtected); + } + + void SetResourcesContent(const ResourcesContent& content) + { + transaction_.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::shared_ptr mainDicomTagsRegistry_; // "shared_ptr" because of PImpl + bool hasFlushToDisk_; + + // Mutex to protect the configuration options + boost::shared_mutex mutex_; + std::unique_ptr factory_; + unsigned int maxRetries_; + + 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: + explicit StatelessDatabaseOperations(IDatabaseWrapper& database); + + void SetTransactionContextFactory(ITransactionContextFactory* factory /* takes ownership */); + + // Only used to handle "ErrorCode_DatabaseCannotSerialize" in the + // case of collision between multiple writers + void SetMaxDatabaseRetries(unsigned int maxRetries); + + // It is assumed that "GetDatabaseVersion()" can run out of a + // database transaction + unsigned int GetDatabaseVersion() + { + return db_.GetDatabaseVersion(); + } + + void FlushToDisk(); + + bool HasFlushToDisk() const + { + return hasFlushToDisk_; + } + + 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, + int64_t& revision, + 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, + int64_t& revision, + 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, + bool shared); + + std::string GetGlobalProperty(GlobalProperty property, + bool shared, + 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(int64_t& newRevision /*out*/, + const std::string& publicId, + MetadataType type, + const std::string& value, + bool hasOldRevision, + int64_t oldRevision); + + // Same as "SetMetadata()", but doesn't care about revisions + void OverwriteMetadata(const std::string& publicId, + MetadataType type, + const std::string& value); + + bool DeleteMetadata(const std::string& publicId, + MetadataType type, + bool hasRevision, + int64_t revision); + + uint64_t IncrementGlobalSequence(GlobalProperty sequence, + bool shared); + + void DeleteChanges(); + + void DeleteExportedResources(); + + void SetGlobalProperty(GlobalProperty property, + bool shared, + const std::string& value); + + bool DeleteAttachment(const std::string& publicId, + FileContentType type, + bool hasRevision, + int64_t revision); + + void LogChange(int64_t internalId, + 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(int64_t& newRevision /*out*/, + const FileInfo& attachment, + const std::string& publicId, + uint64_t maximumStorageSize, + unsigned int maximumPatients, + bool hasOldRevision, + int64_t oldRevision); + }; +} diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/VoidDatabaseListener.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/VoidDatabaseListener.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -0,0 +1,57 @@ +/** + * 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 "VoidDatabaseListener.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + +namespace Orthanc +{ + void VoidDatabaseListener::SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) + { + throw OrthancException(ErrorCode_InternalError); + } + + void VoidDatabaseListener::SignalAttachmentDeleted(const FileInfo& info) + { + throw OrthancException(ErrorCode_InternalError); + } + + void VoidDatabaseListener::SignalResourceDeleted(ResourceType type, + const std::string& publicId) + { + throw OrthancException(ErrorCode_InternalError); + } +} diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/Database/VoidDatabaseListener.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/VoidDatabaseListener.h Tue Apr 20 18:11:29 2021 +0200 @@ -0,0 +1,56 @@ +/** + * 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/Compatibility.h" +#include "IDatabaseListener.h" + +namespace Orthanc +{ + /** + * This is a listener that can be used for transactions that do + * not create/delete attachments or resources. + **/ + class VoidDatabaseListener : public IDatabaseListener + { + public: + virtual void SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) ORTHANC_OVERRIDE; + + virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE; + + virtual void SignalResourceDeleted(ResourceType type, + const std::string& publicId) ORTHANC_OVERRIDE; + }; +} diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/LuaScripting.cpp --- a/OrthancServer/Sources/LuaScripting.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/LuaScripting.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -38,6 +38,7 @@ #include "OrthancRestApi/OrthancRestApi.h" #include "ServerContext.h" +#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/HttpServer/StringHttpOutput.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h" @@ -126,6 +127,82 @@ private: ServerIndexChange change_; + class GetInfoOperations : public ServerIndex::IReadOnlyOperations + { + private: + const ServerIndexChange& change_; + bool ok_; + DicomMap tags_; + std::map metadata_; + + public: + explicit GetInfoOperations(const ServerIndexChange& change) : + change_(change), + ok_(false) + { + } + + virtual void Apply(ServerIndex::ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE + { + int64_t internalId; + ResourceType level; + if (transaction.LookupResource(internalId, level, change_.GetPublicId()) && + level == change_.GetResourceType()) + { + transaction.GetMainDicomTags(tags_, internalId); + transaction.GetAllMetadata(metadata_, internalId); + ok_ = true; + } + } + + void CallLua(LuaScripting& that, + const char* name) const + { + if (ok_) + { + Json::Value formattedMetadata = Json::objectValue; + + for (std::map::const_iterator + it = metadata_.begin(); it != metadata_.end(); ++it) + { + std::string key = EnumerationToString(it->first); + formattedMetadata[key] = it->second; + } + + { + LuaScripting::Lock lock(that); + + if (lock.GetLua().IsExistingFunction(name)) + { + that.InitializeJob(); + + Json::Value json = Json::objectValue; + + if (change_.GetResourceType() == ResourceType_Study) + { + DicomMap t; + tags_.ExtractStudyInformation(t); // Discard patient-related tags + FromDcmtkBridge::ToJson(json, t, true); + } + else + { + FromDcmtkBridge::ToJson(json, tags_, true); + } + + LuaFunctionCall call(lock.GetLua(), name); + call.PushString(change_.GetPublicId()); + call.PushJson(json); + call.PushJson(formattedMetadata); + call.Execute(); + + that.SubmitJob(); + } + } + } + } + }; + + public: explicit StableResourceEvent(const ServerIndexChange& change) : change_(change) @@ -164,39 +241,9 @@ } } - Json::Value tags; - - if (that.context_.GetIndex().LookupResource(tags, change_.GetPublicId(), change_.GetResourceType())) - { - std::map metadata; - that.context_.GetIndex().GetAllMetadata(metadata, change_.GetPublicId(), change_.GetResourceType()); - - Json::Value formattedMetadata = Json::objectValue; - - for (std::map::const_iterator - it = metadata.begin(); it != metadata.end(); ++it) - { - std::string key = EnumerationToString(it->first); - formattedMetadata[key] = it->second; - } - - { - LuaScripting::Lock lock(that); - - if (lock.GetLua().IsExistingFunction(name)) - { - that.InitializeJob(); - - LuaFunctionCall call(lock.GetLua(), name); - call.PushString(change_.GetPublicId()); - call.PushJson(tags["MainDicomTags"]); - call.PushJson(formattedMetadata); - call.Execute(); - - that.SubmitJob(); - } - } - } + GetInfoOperations operations(change_); + that.context_.GetIndex().Apply(operations); + operations.CallLua(that, name); } }; diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/OrthancConfiguration.cpp --- a/OrthancServer/Sources/OrthancConfiguration.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -53,6 +53,7 @@ static const char* const ORTHANC_PEERS = "OrthancPeers"; static const char* const ORTHANC_PEERS_IN_DB = "OrthancPeersInDatabase"; static const char* const TEMPORARY_DIRECTORY = "TemporaryDirectory"; +static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier"; namespace Orthanc { @@ -254,7 +255,7 @@ } else { - std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Modalities, "{}"); + std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Modalities, false /* not shared */, "{}"); Json::Value modalities; if (Toolbox::ReadJson(modalities, property)) @@ -293,7 +294,7 @@ } else { - std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Peers, "{}"); + std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Peers, false /* not shared */, "{}"); Json::Value peers; if (Toolbox::ReadJson(peers, property)) @@ -369,7 +370,7 @@ std::string s; Toolbox::WriteFastJson(s, modalities); - serverIndex_->SetGlobalProperty(GlobalProperty_Modalities, s); + serverIndex_->SetGlobalProperty(GlobalProperty_Modalities, false /* not shared */, s); } } else @@ -401,7 +402,7 @@ std::string s; Toolbox::WriteFastJson(s, peers); - serverIndex_->SetGlobalProperty(GlobalProperty_Peers, s); + serverIndex_->SetGlobalProperty(GlobalProperty_Peers, false /* not shared */, s); } } else @@ -1014,6 +1015,59 @@ } + std::string OrthancConfiguration::GetDatabaseServerIdentifier() const + { + std::string id; + + if (LookupStringParameter(id, DATABASE_SERVER_IDENTIFIER)) + { + if (id.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Global configuration option \"" + + std::string(DATABASE_SERVER_IDENTIFIER) + "\" cannot be empty"); + } + else + { + return id; + } + } + else + { + std::set items; + + { + std::set mac; + SystemToolbox::GetMacAddresses(mac); + + for (std::set::const_iterator it = mac.begin(); it != mac.end(); ++it) + { + items.insert("mac=" + *it); + } + } + + items.insert("aet=" + GetStringParameter("DicomAet", "ORTHANC")); + items.insert("dicom-port=" + boost::lexical_cast(GetUnsignedIntegerParameter("DicomPort", 4242))); + items.insert("http-port=" + boost::lexical_cast(GetUnsignedIntegerParameter("HttpPort", 8042))); + + for (std::set::const_iterator it = items.begin(); it != items.end(); ++it) + { + if (id.empty()) + { + id = *it; + } + else + { + id += ("|" + *it); + } + } + + std::string hash; + Toolbox::ComputeSHA1(hash, id); + return hash; + } + } + + void OrthancConfiguration::DefaultExtractDicomSummary(DicomMap& target, const ParsedDicomFile& dicom) { diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/OrthancConfiguration.h --- a/OrthancServer/Sources/OrthancConfiguration.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.h Tue Apr 20 18:11:29 2021 +0200 @@ -251,6 +251,8 @@ void GetAcceptedTransferSyntaxes(std::set& target) const; + std::string GetDatabaseServerIdentifier() const; + static void DefaultExtractDicomSummary(DicomMap& target, const ParsedDicomFile& dicom); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -53,7 +53,7 @@ static std::string GeneratePatientName(ServerContext& context) { - uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence); + uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true /* shared */); return "Anonymized" + boost::lexical_cast(seq); } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -51,8 +51,6 @@ #include "../ServerToolbox.h" #include "../SliceOrdering.h" -#include "../../Plugins/Engine/OrthancPlugins.h" - // This "include" is mandatory for Release builds using Linux Standard Base #include @@ -65,6 +63,9 @@ static Orthanc::Semaphore throttlingSemaphore_(4); // TODO => PARAMETER? +static const std::string CHECK_REVISIONS = "CheckRevisions"; + + namespace Orthanc { static std::string GetDocumentationSampleResource(ResourceType type) @@ -168,10 +169,10 @@ { if (expand) { - Json::Value item; - if (index.LookupResource(item, *resource, level)) + Json::Value expanded; + if (index.ExpandResource(expanded, *resource, level)) { - answer.append(item); + answer.append(expanded); } } else @@ -255,11 +256,11 @@ .SetHttpGetSample(GetDocumentationSampleResource(resourceType), true); return; } - - Json::Value result; - if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType)) + + Json::Value json; + if (OrthancRestApi::GetIndex(call).ExpandResource(json, call.GetUriComponent("id", ""), resourceType)) { - call.GetOutput().AnswerJson(result); + call.GetOutput().AnswerJson(json); } } @@ -1414,12 +1415,13 @@ return; } - std::map metadata; - assert(!call.GetFullUri().empty()); const std::string publicId = call.GetUriComponent("id", ""); - const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); - + ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); + + typedef std::map Metadata; + + Metadata metadata; OrthancRestApi::GetIndex(call).GetAllMetadata(metadata, publicId, level); Json::Value result; @@ -1428,8 +1430,7 @@ { result = Json::objectValue; - for (std::map::const_iterator - it = metadata.begin(); it != metadata.end(); ++it) + for (Metadata::const_iterator it = metadata.begin(); it != metadata.end(); ++it) { std::string key = EnumerationToString(it->first); result[key] = it->second; @@ -1439,8 +1440,7 @@ { result = Json::arrayValue; - for (std::map::const_iterator - it = metadata.begin(); it != metadata.end(); ++it) + for (Metadata::const_iterator it = metadata.begin(); it != metadata.end(); ++it) { result.append(EnumerationToString(it->first)); } @@ -1450,6 +1450,37 @@ } + static bool GetRevisionHeader(int64_t& revision /* out */, + const RestApiCall& call, + const std::string& header) + { + std::string lower; + Toolbox::ToLowerCase(lower, header); + + HttpToolbox::Arguments::const_iterator found = call.GetHttpHeaders().find(lower); + if (found == call.GetHttpHeaders().end()) + { + return false; + } + else + { + std::string value = Toolbox::StripSpaces(found->second); + Toolbox::RemoveSurroundingQuotes(value); + + try + { + revision = boost::lexical_cast(value); + return true; + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "The \"" + header + + "\" HTTP header should contain the revision as an integer, but found: " + value); + } + } + } + + static void GetMetadata(RestApiGetCall& call) { if (call.IsDocumentation()) @@ -1462,7 +1493,9 @@ .SetDescription("Get the value of a metadata that is associated with the given " + r) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)") - .AddAnswerType(MimeType_PlainText, "Value of the metadata"); + .AddAnswerType(MimeType_PlainText, "Value of the metadata") + .SetAnswerHeader("ETag", "Revision of the metadata, to be used in further `PUT` or `DELETE` operations") + .SetHttpHeader("If-None-Match", "Optional revision of the metadata, to check if its content has changed"); return; } @@ -1474,9 +1507,22 @@ MetadataType metadata = StringToMetadata(name); std::string value; - if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, level, metadata)) + int64_t revision; + if (OrthancRestApi::GetIndex(call).LookupMetadata(value, revision, publicId, level, metadata)) { - call.GetOutput().AnswerBuffer(value, MimeType_PlainText); + call.GetOutput().GetLowLevelOutput(). + AddHeader("ETag", "\"" + boost::lexical_cast(revision) + "\""); // New in Orthanc 1.9.2 + + int64_t userRevision; + if (GetRevisionHeader(userRevision, call, "If-None-Match") && + revision == userRevision) + { + call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified); + } + else + { + call.GetOutput().AnswerBuffer(value, MimeType_PlainText); + } } } @@ -1493,20 +1539,48 @@ .SetDescription("Delete some metadata associated with the given DICOM " + r + ". This call will fail if trying to delete a system metadata (i.e. whose index is < 1024).") .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)"); + .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)") + .SetHttpHeader("If-Match", "Revision of the metadata, to check if its content has not changed and can " + "be deleted. This header is mandatory if `CheckRevision` option is `true`."); return; } CheckValidResourceType(call); - - std::string publicId = call.GetUriComponent("id", ""); + const std::string publicId = call.GetUriComponent("id", ""); + std::string name = call.GetUriComponent("name", ""); MetadataType metadata = StringToMetadata(name); if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata - { - OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata); - call.GetOutput().AnswerBuffer("", MimeType_PlainText); + { + bool found; + int64_t revision; + if (GetRevisionHeader(revision, call, "if-match")) + { + found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, true, revision); + } + else + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) + { + throw OrthancException(ErrorCode_Revision, + "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\""); + } + else + { + found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, false, -1 /* dummy value */); + } + } + + if (found) + { + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } } else { @@ -1528,7 +1602,8 @@ ". This call will fail if trying to modify a system metadata (i.e. whose index is < 1024).") .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)") - .AddRequestType(MimeType_PlainText, "String value of the metadata"); + .AddRequestType(MimeType_PlainText, "String value of the metadata") + .SetHttpHeader("If-Match", "Revision of the metadata, if this is not the first time this metadata is set."); return; } @@ -1543,8 +1618,28 @@ if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata { - // It is forbidden to modify internal metadata - OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value); + int64_t oldRevision; + bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match"); + + if (!hasOldRevision) + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) + { + // "StatelessDatabaseOperations::SetMetadata()" will ignore + // the actual value of "oldRevision" if the metadata is + // inexistent as expected + hasOldRevision = true; + oldRevision = -1; // dummy value + } + } + + int64_t newRevision; + OrthancRestApi::GetIndex(call).SetMetadata(newRevision, publicId, metadata, value, hasOldRevision, oldRevision); + + call.GetOutput().GetLowLevelOutput(). + AddHeader("ETag", "\"" + boost::lexical_cast(newRevision) + "\""); // New in Orthanc 1.9.2 + call.GetOutput().AnswerBuffer("", MimeType_PlainText); } else @@ -1591,15 +1686,48 @@ } - static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call) + static void AddAttachmentDocumentation(RestApiGetCall& call, + const std::string& resourceType) + { + call.GetDocumentation() + .SetUriArgument("id", "Orthanc identifier of the " + resourceType + " of interest") + .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") + .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations") + .SetHttpHeader("If-None-Match", "Optional revision of the attachment, to check if its content has changed"); + } + + + static bool GetAttachmentInfo(FileInfo& info, + RestApiGetCall& call) { CheckValidResourceType(call); - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); + const std::string publicId = call.GetUriComponent("id", ""); + const std::string name = call.GetUriComponent("name", ""); FileContentType contentType = StringToContentType(name); - return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType); + int64_t revision; + if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType)) + { + call.GetOutput().GetLowLevelOutput(). + AddHeader("ETag", "\"" + boost::lexical_cast(revision) + "\""); // New in Orthanc 1.9.2 + + int64_t userRevision; + if (GetRevisionHeader(userRevision, call, "If-None-Match") && + revision == userRevision) + { + call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified); + return false; + } + else + { + return true; + } + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } } @@ -1609,12 +1737,11 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag("Other") .SetSummary("List operations on attachments") .SetDescription("Get the list of the operations that are available for attachments associated with the given " + r) - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_Json, "List of the available operations") .SetHttpGetSample("https://demo.orthanc-server.com/instances/d94d9a03-3003b047-a4affc69-322313b2-680530a2/attachments/dicom", true); return; @@ -1670,7 +1797,9 @@ std::string(uncompress ? "" : ". The attachment will not be decompressed if `StorageCompression` is `true`.")) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") - .AddAnswerType(MimeType_Binary, "The attachment"); + .AddAnswerType(MimeType_Binary, "The attachment") + .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations") + .SetHttpHeader("If-None-Match", "Optional revision of the metadata, to check if its content has changed"); return; } @@ -1683,14 +1812,32 @@ if (uncompress) { - context.AnswerAttachment(call.GetOutput(), publicId, type); + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + context.AnswerAttachment(call.GetOutput(), publicId, type); + } } else { // Return the raw data (possibly compressed), as stored on the filesystem std::string content; - context.ReadAttachment(content, publicId, type, false); - call.GetOutput().AnswerBuffer(content, MimeType_Binary); + int64_t revision; + context.ReadAttachment(content, revision, publicId, type, false); + + call.GetOutput().GetLowLevelOutput(). + AddHeader("ETag", "\"" + boost::lexical_cast(revision) + "\""); // New in Orthanc 1.9.2 + + int64_t userRevision; + if (GetRevisionHeader(userRevision, call, "If-None-Match") && + revision == userRevision) + { + call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified); + } + else + { + call.GetOutput().AnswerBuffer(content, MimeType_Binary); + } } } @@ -1701,12 +1848,11 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Get size of attachment") .SetDescription("Get the size of one attachment associated with the given " + r) - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "The size of the attachment"); return; } @@ -1725,13 +1871,12 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Get size of attachment on disk") .SetDescription("Get the size of one attachment associated with the given " + r + ", as stored on the disk. " "This is different from `.../size` iff `EnableStorage` is `true`.") - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "The size of the attachment, as stored on the disk"); return; } @@ -1750,12 +1895,11 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Get MD5 of attachment") .SetDescription("Get the MD5 hash of one attachment associated with the given " + r) - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment"); return; } @@ -1775,13 +1919,12 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Get MD5 of attachment on disk") .SetDescription("Get the MD5 hash of one attachment associated with the given " + r + ", as stored on the disk. " "This is different from `.../md5` iff `EnableStorage` is `true`.") - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment, as stored on the disk"); return; } @@ -1816,9 +1959,11 @@ std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); + FileContentType contentType = StringToContentType(name); FileInfo info; - if (!GetAttachmentInfo(info, call) || + int64_t revision; // Ignored + if (!OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType) || info.GetCompressedMD5() == "" || info.GetUncompressedMD5() == "") { @@ -1830,7 +1975,7 @@ // First check whether the compressed data is correctly stored in the disk std::string data; - context.ReadAttachment(data, publicId, StringToContentType(name), false); + context.ReadAttachment(data, revision, publicId, StringToContentType(name), false); std::string actualMD5; Toolbox::ComputeMD5(actualMD5, data); @@ -1845,7 +1990,7 @@ } else { - context.ReadAttachment(data, publicId, StringToContentType(name), true); + context.ReadAttachment(data, revision, publicId, StringToContentType(name), true); Toolbox::ComputeMD5(actualMD5, data); ok = (actualMD5 == info.GetUncompressedMD5()); } @@ -1877,7 +2022,8 @@ .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddRequestType(MimeType_Binary, "Binary data containing the attachment") - .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success"); + .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success") + .SetHttpHeader("If-Match", "Revision of the attachment, if this is not the first time this attachment is set."); return; } @@ -1888,9 +2034,31 @@ std::string name = call.GetUriComponent("name", ""); FileContentType contentType = StringToContentType(name); - if (IsUserContentType(contentType) && // It is forbidden to modify internal attachments - context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize())) + if (IsUserContentType(contentType)) // It is forbidden to modify internal attachments { + int64_t oldRevision; + bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match"); + + if (!hasOldRevision) + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) + { + // "StatelessDatabaseOperations::AddAttachment()" will ignore + // the actual value of "oldRevision" if the metadata is + // inexistent as expected + hasOldRevision = true; + oldRevision = -1; // dummy value + } + } + + int64_t newRevision; + context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(), + call.GetBodySize(), hasOldRevision, oldRevision); + + call.GetOutput().GetLowLevelOutput(). + AddHeader("ETag", "\"" + boost::lexical_cast(newRevision) + "\""); // New in Orthanc 1.9.2 + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } else @@ -1912,7 +2080,9 @@ .SetDescription("Delete an attachment associated with the given DICOM " + r + ". This call will fail if trying to delete a system attachment (i.e. whose index is < 1024).") .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)"); + .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") + .SetHttpHeader("If-Match", "Revision of the attachment, to check if its content has not changed and can " + "be deleted. This header is mandatory if `CheckRevision` option is `true`."); return; } @@ -1947,8 +2117,34 @@ if (allowed) { - OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType); - call.GetOutput().AnswerBuffer("{}", MimeType_Json); + bool found; + int64_t revision; + if (GetRevisionHeader(revision, call, "if-match")) + { + found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, true, revision); + } + else + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) + { + throw OrthancException(ErrorCode_Revision, + "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\""); + } + else + { + found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, false, -1 /* dummy value */); + } + } + + if (found) + { + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } } else { @@ -1990,12 +2186,11 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Is attachment compressed?") .SetDescription("Test whether the attachment has been stored as a compressed file on the disk.") - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "`0` if the attachment was stored uncompressed, `1` if it was compressed"); return; } @@ -2544,11 +2739,10 @@ for (std::list::const_iterator it = a.begin(); it != a.end(); ++it) { - Json::Value item; - - if (OrthancRestApi::GetIndex(call).LookupResource(item, *it, end)) + Json::Value resource; + if (OrthancRestApi::GetIndex(call).ExpandResource(resource, *it, end)) { - result.append(item); + result.append(resource); } } @@ -2654,10 +2848,10 @@ assert(currentType == end); - Json::Value result; - if (index.LookupResource(result, current, end)) + Json::Value resource; + if (OrthancRestApi::GetIndex(call).ExpandResource(resource, current, end)) { - call.GetOutput().AnswerJson(result); + call.GetOutput().AnswerJson(resource); } } @@ -2792,7 +2986,7 @@ for (std::list::const_iterator instance = instances.begin(); instance != instances.end(); ++instance) { - index.DeleteAttachment(*instance, FileContentType_DicomAsJson); + index.DeleteAttachment(*instance, FileContentType_DicomAsJson, false /* no revision checks */, -1 /* dummy */); } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -61,29 +61,44 @@ static void GetSystemInformation(RestApiGetCall& call) { + static const char* const API_VERSION = "ApiVersion"; + static const char* const CHECK_REVISIONS = "CheckRevisions"; + static const char* const DATABASE_BACKEND_PLUGIN = "DatabaseBackendPlugin"; + static const char* const DATABASE_VERSION = "DatabaseVersion"; + static const char* const DICOM_AET = "DicomAet"; + static const char* const DICOM_PORT = "DicomPort"; + static const char* const HTTP_PORT = "HttpPort"; + static const char* const IS_HTTP_SERVER_SECURE = "IsHttpServerSecure"; + static const char* const NAME = "Name"; + static const char* const PLUGINS_ENABLED = "PluginsEnabled"; + static const char* const STORAGE_AREA_PLUGIN = "StorageAreaPlugin"; + static const char* const VERSION = "Version"; + if (call.IsDocumentation()) { call.GetDocumentation() .SetTag("System") .SetSummary("Get system information") .SetDescription("Get system information about Orthanc") - .SetAnswerField("ApiVersion", RestApiCallDocumentation::Type_Number, "Version of the REST API") - .SetAnswerField("Version", RestApiCallDocumentation::Type_String, "Version of Orthanc") - .SetAnswerField("DatabaseVersion", RestApiCallDocumentation::Type_Number, + .SetAnswerField(API_VERSION, RestApiCallDocumentation::Type_Number, "Version of the REST API") + .SetAnswerField(VERSION, RestApiCallDocumentation::Type_String, "Version of Orthanc") + .SetAnswerField(DATABASE_VERSION, RestApiCallDocumentation::Type_Number, "Version of the database: https://book.orthanc-server.com/developers/db-versioning.html") - .SetAnswerField("IsHttpServerSecure", RestApiCallDocumentation::Type_Boolean, + .SetAnswerField(IS_HTTP_SERVER_SECURE, RestApiCallDocumentation::Type_Boolean, "Whether the REST API is properly secured (assuming no reverse proxy is in use): https://book.orthanc-server.com/faq/security.html#securing-the-http-server") - .SetAnswerField("StorageAreaPlugin", RestApiCallDocumentation::Type_String, + .SetAnswerField(STORAGE_AREA_PLUGIN, RestApiCallDocumentation::Type_String, "Information about the installed storage area plugin (`null` if no such plugin is installed)") - .SetAnswerField("DatabaseBackendPlugin", RestApiCallDocumentation::Type_String, + .SetAnswerField(DATABASE_BACKEND_PLUGIN, RestApiCallDocumentation::Type_String, "Information about the installed database index plugin (`null` if no such plugin is installed)") - .SetAnswerField("DicomAet", RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc") - .SetAnswerField("DicomPort", RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc") - .SetAnswerField("HttpPort", RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc") - .SetAnswerField("Name", RestApiCallDocumentation::Type_String, + .SetAnswerField(DICOM_AET, RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc") + .SetAnswerField(DICOM_PORT, RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc") + .SetAnswerField(HTTP_PORT, RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc") + .SetAnswerField(NAME, RestApiCallDocumentation::Type_String, "The name of the Orthanc server, cf. the `Name` configuration option") - .SetAnswerField("PluginsEnabled", RestApiCallDocumentation::Type_Boolean, + .SetAnswerField(PLUGINS_ENABLED, RestApiCallDocumentation::Type_Boolean, "Whether Orthanc was built with support for plugins") + .SetAnswerField(CHECK_REVISIONS, RestApiCallDocumentation::Type_Boolean, + "Whether Orthanc handle revisions of metadata and attachments to deal with multiple writers (new in Orthanc 1.9.2)") .SetHttpGetSample("https://demo.orthanc-server.com/system", true); return; } @@ -92,39 +107,40 @@ Json::Value result = Json::objectValue; - result["ApiVersion"] = ORTHANC_API_VERSION; - result["Version"] = ORTHANC_VERSION; - result["DatabaseVersion"] = OrthancRestApi::GetIndex(call).GetDatabaseVersion(); - result["IsHttpServerSecure"] = context.IsHttpServerSecure(); // New in Orthanc 1.5.8 + result[API_VERSION] = ORTHANC_API_VERSION; + result[VERSION] = ORTHANC_VERSION; + result[DATABASE_VERSION] = OrthancRestApi::GetIndex(call).GetDatabaseVersion(); + result[IS_HTTP_SERVER_SECURE] = context.IsHttpServerSecure(); // New in Orthanc 1.5.8 { OrthancConfiguration::ReaderLock lock; - result["DicomAet"] = lock.GetConfiguration().GetOrthancAET(); - result["DicomPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242); - result["HttpPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042); - result["Name"] = lock.GetConfiguration().GetStringParameter("Name", ""); + result[DICOM_AET] = lock.GetConfiguration().GetOrthancAET(); + result[DICOM_PORT] = lock.GetConfiguration().GetUnsignedIntegerParameter(DICOM_PORT, 4242); + result[HTTP_PORT] = lock.GetConfiguration().GetUnsignedIntegerParameter(HTTP_PORT, 8042); + result[NAME] = lock.GetConfiguration().GetStringParameter(NAME, ""); + result[CHECK_REVISIONS] = lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false); // New in Orthanc 1.9.2 } - result["StorageAreaPlugin"] = Json::nullValue; - result["DatabaseBackendPlugin"] = Json::nullValue; + result[STORAGE_AREA_PLUGIN] = Json::nullValue; + result[DATABASE_BACKEND_PLUGIN] = Json::nullValue; #if ORTHANC_ENABLE_PLUGINS == 1 - result["PluginsEnabled"] = true; + result[PLUGINS_ENABLED] = true; const OrthancPlugins& plugins = context.GetPlugins(); if (plugins.HasStorageArea()) { std::string p = plugins.GetStorageAreaLibrary().GetPath(); - result["StorageAreaPlugin"] = boost::filesystem::canonical(p).string(); + result[STORAGE_AREA_PLUGIN] = boost::filesystem::canonical(p).string(); } if (plugins.HasDatabaseBackend()) { std::string p = plugins.GetDatabaseBackendLibrary().GetPath(); - result["DatabaseBackendPlugin"] = boost::filesystem::canonical(p).string(); + result[DATABASE_BACKEND_PLUGIN] = boost::filesystem::canonical(p).string(); } #else - result["PluginsEnabled"] = false; + result[PLUGINS_ENABLED] = false; #endif call.GetOutput().AnswerJson(result); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/OrthancWebDav.cpp --- a/OrthancServer/Sources/OrthancWebDav.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -50,7 +50,6 @@ static const char* const BY_DATES = "by-dates"; static const char* const BY_UIDS = "by-uids"; static const char* const UPLOADS = "uploads"; -static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; static const char* const STUDY_INFO = "study.json"; static const char* const SERIES_INFO = "series.json"; @@ -70,7 +69,8 @@ MetadataType metadata) { std::string value; - if (context.GetIndex().LookupMetadata(value, publicId, level, metadata)) + int64_t revision; // Ignored + if (context.GetIndex().LookupMetadata(value, revision, publicId, level, metadata)) { try { @@ -153,7 +153,8 @@ if (level_ == ResourceType_Instance) { FileInfo info; - if (context_.GetIndex().LookupAttachment(info, publicId, FileContentType_Dicom)) + int64_t revision; // Ignored + if (context_.GetIndex().LookupAttachment(info, revision, publicId, FileContentType_Dicom)) { std::unique_ptr f(new File(s + ".dcm")); f->SetMimeType(MimeType_Dicom); @@ -268,8 +269,8 @@ const DicomMap& mainDicomTags, const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE { - Json::Value info; - if (context_.GetIndex().LookupResource(info, publicId, level_)) + Json::Value resource; + if (context_.GetIndex().ExpandResource(resource, publicId, level_)) { if (success_) { @@ -277,7 +278,7 @@ } else { - target_ = info.toStyledString(); + target_ = resource.toStyledString(); // Replace UNIX newlines with DOS newlines boost::replace_all(target_, "\n", "\r\n"); @@ -508,7 +509,8 @@ LookupTime(time, context_, *it, ResourceType_Instance, MetadataType_Instance_ReceptionDate); FileInfo info; - if (context_.GetIndex().LookupAttachment(info, *it, FileContentType_Dicom)) + int64_t revision; // Ignored + if (context_.GetIndex().LookupAttachment(info, revision, *it, FileContentType_Dicom)) { std::unique_ptr resource(new File(*it + ".dcm")); resource->SetMimeType(MimeType_Dicom); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -215,7 +215,7 @@ if (loadJobsFromDatabase) { std::string serialized; - if (index_.LookupGlobalProperty(serialized, GlobalProperty_JobsRegistry)) + if (index_.LookupGlobalProperty(serialized, GlobalProperty_JobsRegistry, false /* not shared */)) { LOG(WARNING) << "Reloading the jobs from the last execution of Orthanc"; @@ -261,7 +261,7 @@ std::string serialized; Toolbox::WriteFastJson(serialized, value); - index_.SetGlobalProperty(GlobalProperty_JobsRegistry, serialized); + index_.SetGlobalProperty(GlobalProperty_JobsRegistry, false /* not shared */, serialized); } catch (OrthancException& e) { @@ -753,13 +753,16 @@ FileContentType content) { FileInfo attachment; - if (!index_.LookupAttachment(attachment, resourceId, content)) + int64_t revision; + if (!index_.LookupAttachment(attachment, revision, resourceId, content)) { throw OrthancException(ErrorCode_UnknownResource); } - - StorageAccessor accessor(area_, GetMetricsRegistry()); - accessor.AnswerFile(output, attachment, GetFileContentMime(content)); + else + { + StorageAccessor accessor(area_, GetMetricsRegistry()); + accessor.AnswerFile(output, attachment, GetFileContentMime(content)); + } } @@ -773,7 +776,8 @@ << compression; FileInfo attachment; - if (!index_.LookupAttachment(attachment, resourceId, attachmentType)) + int64_t revision; + if (!index_.LookupAttachment(attachment, revision, resourceId, attachmentType)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -794,7 +798,8 @@ try { - StoreStatus status = index_.AddAttachment(modified, resourceId); + int64_t newRevision; // ignored + StoreStatus status = index_.AddAttachment(newRevision, modified, resourceId, true, revision); if (status != StoreStatus_Success) { accessor.Remove(modified); @@ -833,8 +838,9 @@ **/ FileInfo attachment; + int64_t revision; // Ignored - if (index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomUntilPixelData)) + if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData)) { std::string dicom; @@ -860,7 +866,7 @@ { std::string s; - if (index_.LookupMetadata(s, instancePublicId, ResourceType_Instance, + if (index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance, MetadataType_Instance_PixelDataOffset)) { hasPixelDataOffset = false; @@ -892,7 +898,7 @@ if (hasPixelDataOffset && area_.HasReadRange() && - index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom) && + index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom) && attachment.GetCompressionType() == CompressionType_None) { /** @@ -921,7 +927,7 @@ } } else if (ignoreTagLength.empty() && - index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomAsJson)) + index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomAsJson)) { /** * CASE 3: This instance was created using Orthanc <= @@ -971,14 +977,16 @@ if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, dicom) && pixelDataOffset < dicom.size()) { - index_.SetMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset, - boost::lexical_cast(pixelDataOffset)); + index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset, + boost::lexical_cast(pixelDataOffset)); if (!area_.HasReadRange() || compressionEnabled_) { - AddAttachment(instancePublicId, FileContentType_DicomUntilPixelData, - dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset); + int64_t newRevision; + AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData, + dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset, + false /* no old revision */, -1 /* dummy revision */); } } } @@ -998,7 +1006,8 @@ void ServerContext::ReadDicom(std::string& dicom, const std::string& instancePublicId) { - ReadAttachment(dicom, instancePublicId, FileContentType_Dicom, true /* uncompress */); + int64_t revision; + ReadAttachment(dicom, revision, instancePublicId, FileContentType_Dicom, true /* uncompress */); } @@ -1011,7 +1020,8 @@ } FileInfo attachment; - if (!index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom)) + int64_t revision; // Ignored + if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom)) { throw OrthancException(ErrorCode_InternalError, "Unable to read the DICOM file of instance " + instancePublicId); @@ -1020,7 +1030,7 @@ std::string s; if (attachment.GetCompressionType() == CompressionType_None && - index_.LookupMetadata(s, instancePublicId, ResourceType_Instance, + index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance, MetadataType_Instance_PixelDataOffset) && !s.empty()) { @@ -1044,12 +1054,13 @@ void ServerContext::ReadAttachment(std::string& result, + int64_t& revision, const std::string& instancePublicId, FileContentType content, bool uncompressIfNeeded) { FileInfo attachment; - if (!index_.LookupAttachment(attachment, instancePublicId, content)) + if (!index_.LookupAttachment(attachment, revision, instancePublicId, content)) { throw OrthancException(ErrorCode_InternalError, "Unable to read attachment " + EnumerationToString(content) + @@ -1145,10 +1156,13 @@ } - bool ServerContext::AddAttachment(const std::string& resourceId, + bool ServerContext::AddAttachment(int64_t& newRevision, + const std::string& resourceId, FileContentType attachmentType, const void* data, - size_t size) + size_t size, + bool hasOldRevision, + int64_t oldRevision) { LOG(INFO) << "Adding attachment " << EnumerationToString(attachmentType) << " to resource " << resourceId; @@ -1158,7 +1172,7 @@ StorageAccessor accessor(area_, GetMetricsRegistry()); FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_); - StoreStatus status = index_.AddAttachment(attachment, resourceId); + StoreStatus status = index_.AddAttachment(newRevision, attachment, resourceId, hasOldRevision, oldRevision); if (status != StoreStatus_Success) { accessor.Remove(attachment); @@ -1643,7 +1657,8 @@ if (metadata == MetadataType_Instance_SopClassUid || metadata == MetadataType_Instance_TransferSyntax) { - if (index_.LookupMetadata(target, publicId, level, metadata)) + int64_t revision; // Ignored + if (index_.LookupMetadata(target, revision, publicId, level, metadata)) { return true; } @@ -1685,7 +1700,7 @@ target = value->GetContent(); // Store for reuse - index_.SetMetadata(publicId, metadata, target); + index_.OverwriteMetadata(publicId, metadata, target); return true; } else @@ -1698,7 +1713,8 @@ else { // No backward - return index_.LookupMetadata(target, publicId, level, metadata); + int64_t revision; // Ignored + return index_.LookupMetadata(target, revision, publicId, level, metadata); } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/ServerContext.h Tue Apr 20 18:11:29 2021 +0200 @@ -295,10 +295,13 @@ return compressionEnabled_; } - bool AddAttachment(const std::string& resourceId, + bool AddAttachment(int64_t& newRevision, + const std::string& resourceId, FileContentType attachmentType, const void* data, - size_t size); + size_t size, + bool hasOldRevision, + int64_t oldRevision); StoreStatus Store(std::string& resultPublicId, DicomInstanceToStore& dicom, @@ -327,6 +330,7 @@ // This method is for low-level operations on "/instances/.../attachments/..." void ReadAttachment(std::string& result, + int64_t& revision, const std::string& instancePublicId, FileContentType content, bool uncompressIfNeeded); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/ServerEnumerations.h --- a/OrthancServer/Sources/ServerEnumerations.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Tue Apr 20 18:11:29 2021 +0200 @@ -109,6 +109,12 @@ TransferSyntaxGroup_H265 // New in Orthanc 1.9.0 }; + enum TransactionType + { + TransactionType_ReadOnly, + TransactionType_ReadWrite + }; + /** * WARNING: Do not change the explicit values in the enumerations @@ -119,7 +125,7 @@ enum GlobalProperty { GlobalProperty_DatabaseSchemaVersion = 1, // Unused in the Orthanc core as of Orthanc 0.9.5 - GlobalProperty_FlushSleep = 2, + GlobalProperty_FlushSleep = 2, // Unused in the Orthanc core if Orthanc > 1.9.1 GlobalProperty_AnonymizationSequence = 3, GlobalProperty_JobsRegistry = 5, GlobalProperty_GetTotalSizeIsFast = 6, // New in Orthanc 1.5.2 diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/ServerIndex.cpp --- a/OrthancServer/Sources/ServerIndex.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -38,45 +38,20 @@ #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 -#include -#include static const uint64_t MEGA_BYTES = 1024 * 1024; namespace Orthanc { - 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 ServerIndex::Listener : public IDatabaseListener + class ServerIndex::TransactionContext : public StatelessDatabaseOperations::ITransactionContext { private: struct FileToRemove @@ -110,41 +85,16 @@ std::list pendingFilesToRemove_; std::list pendingChanges_; uint64_t sizeOfFilesToRemove_; - bool insideTransaction_; + uint64_t sizeOfAddedAttachments_; void Reset() { sizeOfFilesToRemove_ = 0; hasRemainingLevel_ = false; + remainingType_ = ResourceType_Instance; // dummy initialization 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_; + sizeOfAddedAttachments_ = 0; } void CommitFilesToRemove() @@ -175,8 +125,18 @@ } } + public: + explicit TransactionContext(ServerContext& context) : + context_(context) + { + Reset(); + assert(ResourceType_Patient < ResourceType_Study && + ResourceType_Study < ResourceType_Series && + ResourceType_Series < ResourceType_Instance); + } + virtual void SignalRemainingAncestor(ResourceType parentType, - const std::string& publicId) + const std::string& publicId) ORTHANC_OVERRIDE { LOG(TRACE) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")"; @@ -196,99 +156,99 @@ } } - virtual void SignalFileDeleted(const FileInfo& info) + virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE { assert(Toolbox::IsUuid(info.GetUuid())); pendingFilesToRemove_.push_back(FileToRemove(info)); sizeOfFilesToRemove_ += info.GetCompressedSize(); } - virtual void SignalChange(const ServerIndexChange& change) + virtual void SignalResourceDeleted(ResourceType type, + const std::string& publicId) ORTHANC_OVERRIDE + { + SignalChange(ServerIndexChange(ChangeType_Deleted, type, publicId)); + } + + virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE { LOG(TRACE) << "Change related to resource " << change.GetPublicId() << " of type " << EnumerationToString(change.GetResourceType()) << ": " << EnumerationToString(change.GetChangeType()); - if (insideTransaction_) + pendingChanges_.push_back(change); + } + + virtual void SignalAttachmentsAdded(uint64_t compressedSize) ORTHANC_OVERRIDE + { + sizeOfAddedAttachments_ += compressedSize; + } + + virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */, + ResourceType& remainingLevel /* out */) ORTHANC_OVERRIDE + { + if (hasRemainingLevel_) { - pendingChanges_.push_back(change); + remainingPublicId = remainingPublicId_; + remainingLevel = remainingType_; + return true; } else { - context_.SignalChange(change); - } + return false; + } + }; + + virtual void MarkAsUnstable(int64_t id, + Orthanc::ResourceType type, + const std::string& publicId) ORTHANC_OVERRIDE + { + context_.GetIndex().MarkAsUnstable(id, type, publicId); } - bool HasRemainingLevel() const + virtual bool IsUnstableResource(int64_t id) ORTHANC_OVERRIDE { - return hasRemainingLevel_; - } - - ResourceType GetRemainingType() const - { - assert(HasRemainingLevel()); - return remainingType_; + return context_.GetIndex().IsUnstableResource(id); } - const std::string& GetRemainingPublicId() const + virtual void Commit() ORTHANC_OVERRIDE { - assert(HasRemainingLevel()); - return remainingPublicId_; - } - }; - - - class ServerIndex::Transaction - { - private: - ServerIndex& index_; - std::unique_ptr transaction_; - bool isCommitted_; - - public: - explicit Transaction(ServerIndex& index) : - index_(index), - isCommitted_(false) - { - transaction_.reset(index_.db_.StartTransaction()); - transaction_->Begin(); - - index_.listener_->StartTransaction(); + // We can remove the files once the SQLite transaction has + // been successfully committed. Some files might have to be + // deleted because of recycling. + CommitFilesToRemove(); + + // Send all the pending changes to the Orthanc plugins + CommitChanges(); } - ~Transaction() - { - index_.listener_->EndTransaction(); - - if (!isCommitted_) - { - transaction_->Rollback(); - } - } - - void Commit(uint64_t sizeOfAddedFiles) + virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE { - if (!isCommitted_) - { - int64_t delta = (static_cast(sizeOfAddedFiles) - - static_cast(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; - } + return (static_cast(sizeOfAddedAttachments_) - + static_cast(sizeOfFilesToRemove_)); } }; + class ServerIndex::TransactionContextFactory : public ITransactionContextFactory + { + private: + ServerContext& context_; + + public: + explicit TransactionContextFactory(ServerContext& context) : + context_(context) + { + } + + virtual ITransactionContext* Create() + { + // There can be concurrent calls to this method, which is not an + // issue because we simply create an object + return new TransactionContext(context_); + } + }; + + class ServerIndex::UnstableResourcePayload { private: @@ -326,379 +286,71 @@ }; - class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable + void ServerIndex::FlushThread(ServerIndex* that, + unsigned int threadSleepGranularityMilliseconds) { - 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; - + // By default, wait for 10 seconds before flushing + static const unsigned int SLEEP_SECONDS = 10; - Registry registry_; - - void LoadTags(ResourceType level) + if (threadSleepGranularityMilliseconds > 1000) { - { - 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); - } - } - } + throw OrthancException(ErrorCode_ParameterOutOfRange); } - 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(sleepString); - } - } - catch (boost::bad_lexical_cast&) - { - } - - LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")"; + LOG(INFO) << "Starting the database flushing thread (sleep = " << SLEEP_SECONDS << " seconds)"; unsigned int count = 0; + unsigned int countThreshold = (1000 * SLEEP_SECONDS) / threadSleepGranularityMilliseconds; while (!that->done_) { - boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep)); + boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleepGranularityMilliseconds)); count++; - if (count < sleep) + + if (count >= countThreshold) { - continue; + Logging::Flush(); + that->FlushToDisk(); + + count = 0; } - - 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(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; - } - - - - - static bool LookupStringMetadata(std::string& result, - const std::map& metadata, - MetadataType type) + bool ServerIndex::IsUnstableResource(int64_t id) { - 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; - } + boost::mutex::scoped_lock lock(monitoringMutex_); + return unstableResources_.Contains(id); } - 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(oldValue); - } - catch (boost::bad_lexical_cast&) - { - LOG(ERROR) << "Cannot read the global sequence " - << boost::lexical_cast(property) << ", resetting it"; - oldNumber = 0; - } - - db_.SetGlobalProperty(property, boost::lexical_cast(oldNumber + 1)); - return oldNumber + 1; - } - else - { - // Initialize the sequence at "1" - db_.SetGlobalProperty(property, "1"); - return 1; - } - } - - - ServerIndex::ServerIndex(ServerContext& context, IDatabaseWrapper& db, - unsigned int threadSleep) : + unsigned int threadSleepGranularityMilliseconds) : + StatelessDatabaseOperations(db), done_(false), - db_(db), maximumStorageSize_(0), - maximumPatients_(0), - mainDicomTagsRegistry_(new MainDicomTagsRegistry) + maximumPatients_(0) { - listener_.reset(new Listener(context)); - db_.SetListener(*listener_); + SetTransactionContextFactory(new TransactionContextFactory(context)); // Initial recycling if the parameters have changed since the last // execution of Orthanc - StandaloneRecycling(); + StandaloneRecycling(maximumStorageSize_, maximumPatients_); - if (db.HasFlushToDisk()) + if (HasFlushToDisk()) { - flushThread_ = boost::thread(FlushThread, this, threadSleep); + flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds); } unstableResourcesMonitorThread_ = boost::thread - (UnstableResourcesMonitorThread, this, threadSleep); + (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds); } - ServerIndex::~ServerIndex() { if (!done_) @@ -709,15 +361,13 @@ } - void ServerIndex::Stop() { if (!done_) { done_ = true; - if (db_.HasFlushToDisk() && - flushThread_.joinable()) + if (flushThread_.joinable()) { flushThread_.join(); } @@ -730,1373 +380,48 @@ } - 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; - } - - - 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& 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(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())); - } - } - - - db_.SetResourcesContent(content); - } - - - // Check whether the series of this new instance is now completed - int64_t expectedNumberOfInstances; - if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary)) - { - SeriesStatus seriesStatus = GetSeriesStatus(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; - } - - - 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) - { - boost::mutex::scoped_lock lock(mutex_); - diskSize = db_.GetTotalCompressedSize(); - uncompressedSize = db_.GetTotalUncompressedSize(); - countPatients = db_.GetResourceCount(ResourceType_Patient); - countStudies = db_.GetResourceCount(ResourceType_Study); - countSeries = db_.GetResourceCount(ResourceType_Series); - countInstances = db_.GetResourceCount(ResourceType_Instance); - } - - - SeriesStatus ServerIndex::GetSeriesStatus(int64_t id, - int64_t expectedNumberOfInstances) + void ServerIndex::SetMaximumPatientCount(unsigned int count) { - 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; - } + boost::mutex::scoped_lock lock(monitoringMutex_); + maximumPatients_ = count; - 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 ServerIndex::MainDicomTagsToJson(Json::Value& target, - 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); - } - } - - - bool ServerIndex::LookupResource(Json::Value& result, - const std::string& publicId, - ResourceType expectedType) - { - result = Json::objectValue; - - boost::mutex::scoped_lock lock(mutex_); - - // Lookup for the requested resource - int64_t id; - ResourceType type; - std::string parent; - if (!db_.LookupResourceAndParent(id, type, parent, publicId) || - type != expectedType) - { - return false; - } - - // 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: - result["ParentPatient"] = parent; - break; - - case ResourceType_Series: - result["ParentStudy"] = parent; - break; - - case ResourceType_Instance: - result["ParentSeries"] = parent; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - // List the children resources - std::list children; - db_.GetChildrenPublicId(children, id); - - 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) + if (count == 0) { - case ResourceType_Patient: - result["Studies"] = c; - break; - - case ResourceType_Study: - result["Series"] = c; - break; - - case ResourceType_Series: - result["Instances"] = c; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - // Extract the metadata - std::map metadata; - db_.GetAllMetadata(metadata, id); - - // Set the resource type - switch (type) - { - case ResourceType_Patient: - result["Type"] = "Patient"; - break; - - case ResourceType_Study: - result["Type"] = "Study"; - break; - - case ResourceType_Series: - { - result["Type"] = "Series"; - - int64_t i; - if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances)) - { - result["ExpectedNumberOfInstances"] = static_cast(i); - result["Status"] = EnumerationToString(GetSeriesStatus(id, i)); - } - else - { - result["ExpectedNumberOfInstances"] = Json::nullValue; - result["Status"] = EnumerationToString(SeriesStatus_Unknown); - } - - break; - } - - case ResourceType_Instance: - { - result["Type"] = "Instance"; - - FileInfo attachment; - if (!db_.LookupAttachment(attachment, id, FileContentType_Dicom)) - { - throw OrthancException(ErrorCode_InternalError); - } - - result["FileSize"] = static_cast(attachment.GetUncompressedSize()); - result["FileUuid"] = attachment.GetUuid(); - - int64_t i; - if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries)) - { - result["IndexInSeries"] = static_cast(i); - } - else - { - result["IndexInSeries"] = Json::nullValue; - } - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - // Record the remaining information - result["ID"] = publicId; - MainDicomTagsToJson(result, id, type); - - std::string tmp; - - if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom)) - { - result["AnonymizedFrom"] = tmp; - } - - if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom)) - { - result["ModifiedFrom"] = tmp; - } - - if (type == ResourceType_Patient || - type == ResourceType_Study || - type == ResourceType_Series) - { - result["IsStable"] = !unstableResources_.Contains(id); - - if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate)) - { - result["LastUpdate"] = tmp; - } - } - - return true; - } - - - bool ServerIndex::LookupAttachment(FileInfo& attachment, - const std::string& instanceUuid, - FileContentType contentType) - { - boost::mutex::scoped_lock lock(mutex_); - - int64_t id; - ResourceType type; - if (!db_.LookupResource(id, type, instanceUuid)) - { - throw OrthancException(ErrorCode_UnknownResource); - } - - if (db_.LookupAttachment(attachment, id, contentType)) - { - assert(attachment.GetContentType() == contentType); - return true; - } - else - { - return false; - } - } - - - - void ServerIndex::GetAllUuids(std::list& target, - ResourceType resourceType) - { - boost::mutex::scoped_lock lock(mutex_); - db_.GetAllPublicIds(target, resourceType); - } - - - void ServerIndex::GetAllUuids(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) - { - if (limit == 0) - { - target.clear(); - return; - } - - boost::mutex::scoped_lock lock(mutex_); - db_.GetAllPublicIds(target, resourceType, since, limit); - } - - - 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; + LOG(WARNING) << "No limit on the number of stored patients"; } else { - last = log.back().GetSeq(); - } - } - - target["Last"] = static_cast(last); - } - - - void ServerIndex::GetChanges(Json::Value& target, - int64_t since, - unsigned int maxResults) - { - std::list changes; - bool done; - bool hasLast = false; - int64_t last = 0; - - { - boost::mutex::scoped_lock lock(mutex_); - - // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as - // "GetLastChange()" involves calls to "GetPublicId()" - Transaction transaction(*this); - - db_.GetChanges(changes, done, since, maxResults); - if (changes.empty()) - { - last = db_.GetLastChangeIndex(); - hasLast = true; - } - - transaction.Commit(0); - } - - FormatLog(target, changes, "Changes", done, since, hasLast, last); - } - - - void ServerIndex::GetLastChange(Json::Value& target) - { - std::list changes; - bool hasLast = false; - int64_t last = 0; - - { - boost::mutex::scoped_lock lock(mutex_); - - // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as - // "GetLastChange()" involves calls to "GetPublicId()" - Transaction transaction(*this); - - db_.GetLastChange(changes); - if (changes.empty()) - { - last = db_.GetLastChangeIndex(); - hasLast = true; - } - - transaction.Commit(0); - } - - FormatLog(target, changes, "Changes", true, 0, hasLast, 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); + LOG(WARNING) << "At most " << count << " patients will be stored"; } } - 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); - } - - - void ServerIndex::GetExportedResources(Json::Value& target, - int64_t since, - unsigned int maxResults) - { - std::list exported; - bool done; - - { - boost::mutex::scoped_lock lock(mutex_); - db_.GetExportedResources(exported, done, since, maxResults); - } - - FormatLog(target, exported, "Exports", done, since, false, -1); - } - - - void ServerIndex::GetLastExportedResource(Json::Value& target) - { - std::list exported; - - { - boost::mutex::scoped_lock lock(mutex_); - db_.GetLastExportedResource(exported); - } - - FormatLog(target, exported, "Exports", true, 0, false, -1); - } - - - 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; + StandaloneRecycling(maximumStorageSize_, maximumPatients_); } - 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& 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 tmp; - db_.GetChildrenInternalId(tmp, resource); - - for (std::list::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - result.push_back(db_.GetPublicId(*it)); - } - } - - - void ServerIndex::GetChildInstances(std::list& 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 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(); - - if (db_.GetResourceType(resource) == ResourceType_Instance) + boost::mutex::scoped_lock lock(monitoringMutex_); + maximumStorageSize_ = size; + + if (size == 0) { - result.push_back(db_.GetPublicId(resource)); + LOG(WARNING) << "No limit on the size of the storage area"; } else { - // Tag all the children of this resource as to be explored - db_.GetChildrenInternalId(tmp, resource); - for (std::list::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::GetAllMetadata(std::map& 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); - } - - return db_.GetAllMetadata(target, id); - } - - - void ServerIndex::ListAvailableAttachments(std::set& 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 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 f; - db_.ListAvailableAttachments(f, resource); - - for (std::set::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 tmp; - db_.GetChildrenInternalId(tmp, resource); - for (std::list::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - toExplore.push(*it); - } + LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area"; } } - if (countStudies == 0) - { - countStudies = 1; - } - - if (countSeries == 0) - { - countSeries = 1; - } + StandaloneRecycling(maximumStorageSize_, maximumPatients_); } void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that, - unsigned int threadSleep) + unsigned int threadSleepGranularityMilliseconds) { int stableAge; @@ -2115,42 +440,62 @@ while (!that->done_) { // Check for stable resources each few seconds - boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep)); + boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleepGranularityMilliseconds)); - boost::mutex::scoped_lock lock(that->mutex_); - - while (!that->unstableResources_.IsEmpty() && - that->unstableResources_.GetOldestPayload().GetAge() > static_cast(stableAge)) + for (;;) { - // 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); + UnstableResourcePayload stableResource; + int64_t stableId; + + { + boost::mutex::scoped_lock lock(that->monitoringMutex_); - // Ensure that the resource is still existing before logging the change - if (that->db_.IsExistingResource(id)) + if (!that->unstableResources_.IsEmpty() && + that->unstableResources_.GetOldestPayload().GetAge() > static_cast(stableAge)) + { + // This DICOM resource has not received any new instance for + // some time. It can be considered as stable. + stableId = that->unstableResources_.RemoveOldest(stableResource); + //LOG(TRACE) << "Stable resource: " << EnumerationToString(stableResource.GetResourceType()) << " " << stableId; + } + else + { + // No more stable DICOM resource, leave the internal loop + break; + } + } + + try { - switch (payload.GetResourceType()) + /** + * WARNING: Don't protect the calls to "LogChange()" using + * "monitoringMutex_", as this could lead to deadlocks in + * other threads (typically, if "Store()" is being running in + * another thread, which leads to calls to "MarkAsUnstable()", + * which leads to two lockings of "monitoringMutex_"). + **/ + switch (stableResource.GetResourceType()) { case ResourceType_Patient: - that->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId()); + that->LogChange(stableId, ChangeType_StablePatient, stableResource.GetPublicId(), ResourceType_Patient); break; - + case ResourceType_Study: - that->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId()); + that->LogChange(stableId, ChangeType_StableStudy, stableResource.GetPublicId(), ResourceType_Study); break; - + case ResourceType_Series: - that->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId()); + that->LogChange(stableId, ChangeType_StableSeries, stableResource.GetPublicId(), ResourceType_Series); break; - + default: throw OrthancException(ErrorCode_InternalError); } - - //LOG(INFO) << "Stable resource: " << EnumerationToString(payload.type_) << " " << id; } + catch (OrthancException& e) + { + LOG(ERROR) << "Cannot log a change about a stable resource into the database"; + } } } @@ -2162,464 +507,61 @@ 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& 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)); - - std::list 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; + boost::mutex::scoped_lock lock(monitoringMutex_); + UnstableResourcePayload payload(type, publicId); + unstableResources_.AddOrMakeMostRecent(id, payload); + //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id; } } - bool ServerIndex::GetMainDicomTags(DicomMap& result, - const std::string& publicId, - ResourceType expectedType, - ResourceType levelOfInterest) + StoreStatus ServerIndex::Store(std::map& instanceMetadata, + const DicomMap& dicomSummary, + const ServerIndex::Attachments& attachments, + const ServerIndex::MetadataMap& metadata, + const DicomInstanceOrigin& origin, + bool overwrite, + bool hasTransferSyntax, + DicomTransferSyntax transferSyntax, + bool hasPixelDataOffset, + uint64_t pixelDataOffset) { - // 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)) + uint64_t maximumStorageSize; + unsigned int maximumPatients; + { - 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; + boost::mutex::scoped_lock lock(monitoringMutex_); + maximumStorageSize = maximumStorageSize_; + maximumPatients = maximumPatients_; } - 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 patientTags; - tmp.GetTags(patientTags); - - for (std::set::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(); + return StatelessDatabaseOperations::Store( + instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite, hasTransferSyntax, + transferSyntax, hasPixelDataOffset, pixelDataOffset, maximumStorageSize, maximumPatients); } - - bool ServerIndex::LookupParent(std::string& target, - const std::string& publicId, - ResourceType parentType) + + StoreStatus ServerIndex::AddAttachment(int64_t& newRevision, + const FileInfo& attachment, + const std::string& publicId, + bool hasOldRevision, + int64_t oldRevision) { - boost::mutex::scoped_lock lock(mutex_); - - ResourceType type; - int64_t id; - if (!db_.LookupResource(id, type, publicId)) + uint64_t maximumStorageSize; + unsigned int maximumPatients; + { - 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); + boost::mutex::scoped_lock lock(monitoringMutex_); + maximumStorageSize = maximumStorageSize_; + maximumPatients = maximumPatients_; } - 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& 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& resourcesId, - std::vector* instancesId, - const DatabaseLookup& lookup, - ResourceType queryLevel, - size_t limit) - { - std::vector normalized; - NormalizeLookup(normalized, lookup, queryLevel); - - std::list 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); - } + return StatelessDatabaseOperations::AddAttachment( + newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision); } } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/ServerIndex.h --- a/OrthancServer/Sources/ServerIndex.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/ServerIndex.h Tue Apr 20 18:11:29 2021 +0200 @@ -33,45 +33,31 @@ #pragma once +#include "Database/StatelessDatabaseOperations.h" #include "../../OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h" -#include "../../OrthancFramework/Sources/DicomFormat/DicomMap.h" - -#include "Database/IDatabaseWrapper.h" -#include "DicomInstanceOrigin.h" #include -#include namespace Orthanc { - class DatabaseLookup; - class ParsedDicomFile; class ServerContext; - - class ServerIndex : public boost::noncopyable + + class ServerIndex : public StatelessDatabaseOperations { - public: - typedef std::list Attachments; - typedef std::map, std::string> MetadataMap; - private: - class Listener; - class Transaction; + class TransactionContext; + class TransactionContextFactory; class UnstableResourcePayload; - class MainDicomTagsRegistry; bool done_; - boost::mutex mutex_; + boost::mutex monitoringMutex_; boost::thread flushThread_; boost::thread unstableResourcesMonitorThread_; - std::unique_ptr listener_; - IDatabaseWrapper& db_; LeastRecentlyUsedIndex unstableResources_; uint64_t maximumStorageSize_; unsigned int maximumPatients_; - std::unique_ptr mainDicomTagsRegistry_; static void FlushThread(ServerIndex* that, unsigned int threadSleep); @@ -79,59 +65,21 @@ static void UnstableResourcesMonitorThread(ServerIndex* that, unsigned int threadSleep); - void MainDicomTagsToJson(Json::Value& result, - int64_t resourceId, - ResourceType resourceType); - - bool IsRecyclingNeeded(uint64_t instanceSize); - - void Recycle(uint64_t instanceSize, - const std::string& newPatientId); - - void StandaloneRecycling(); - void MarkAsUnstable(int64_t id, Orthanc::ResourceType type, const std::string& publicId); - void LogChange(int64_t internalId, - ChangeType changeType, - ResourceType resourceType, - const std::string& publicId); - - void SignalNewResource(ChangeType changeType, - ResourceType level, - const std::string& publicId, - int64_t internalId); - - uint64_t IncrementGlobalSequenceInternal(GlobalProperty property); - - void NormalizeLookup(std::vector& target, - const DatabaseLookup& source, - ResourceType level) const; - - SeriesStatus GetSeriesStatus(int64_t id, - int64_t expectedNumberOfInstances); + bool IsUnstableResource(int64_t id); public: ServerIndex(ServerContext& context, IDatabaseWrapper& database, - unsigned int threadSleep); + unsigned int threadSleepGranularityMilliseconds); ~ServerIndex(); void Stop(); - uint64_t GetMaximumStorageSize() const - { - return maximumStorageSize_; - } - - uint64_t GetMaximumPatientCount() const - { - return maximumPatients_; - } - // "size == 0" means no limit on the storage size void SetMaximumStorageSize(uint64_t size); @@ -149,145 +97,10 @@ bool hasPixelDataOffset, uint64_t pixelDataOffset); - 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 LookupResource(Json::Value& result, - const std::string& publicId, - ResourceType expectedType); - - bool LookupAttachment(FileInfo& attachment, - const std::string& instanceUuid, - FileContentType contentType); - - void GetAllUuids(std::list& target, - ResourceType resourceType); - - void GetAllUuids(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit); - - bool DeleteResource(Json::Value& target /* out */, - const std::string& uuid, - ResourceType expectedType); - - void GetChanges(Json::Value& target, - int64_t since, - unsigned int maxResults); - - void GetLastChange(Json::Value& target); - - void LogExportedResource(const std::string& publicId, - const std::string& remoteModality); - - void GetExportedResources(Json::Value& target, - int64_t since, - unsigned int maxResults); - - void GetLastExportedResource(Json::Value& target); - - bool IsProtectedPatient(const std::string& publicId); - - void SetProtectedPatient(const std::string& publicId, - bool isProtected); - - void GetChildren(std::list& result, - const std::string& publicId); - - void GetChildInstances(std::list& result, - const std::string& publicId); - - void SetMetadata(const std::string& publicId, - MetadataType type, - const std::string& value); - - void DeleteMetadata(const std::string& publicId, - MetadataType type); - - void GetAllMetadata(std::map& target, - const std::string& publicId, - ResourceType expectedType); - - 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); - - uint64_t IncrementGlobalSequence(GlobalProperty sequence); - - void LogChange(ChangeType changeType, - const std::string& publicId); - - void DeleteChanges(); - - void DeleteExportedResources(); - - 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); - - StoreStatus AddAttachment(const FileInfo& attachment, - const std::string& publicId); - - void DeleteAttachment(const std::string& publicId, - FileContentType type); - - void SetGlobalProperty(GlobalProperty property, - 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); - - unsigned int GetDatabaseVersion(); - - bool LookupParent(std::string& target, - const std::string& publicId, - ResourceType parentType); - - void ReconstructInstance(const ParsedDicomFile& dicom); - - void ApplyLookupResources(std::vector& resourcesId, - std::vector* instancesId, // Can be NULL if not needed - const DatabaseLookup& lookup, - ResourceType queryLevel, - size_t limit); + StoreStatus AddAttachment(int64_t& newRevision /*out*/, + const FileInfo& attachment, + const std::string& publicId, + bool hasOldRevision, + int64_t oldRevision); }; } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/ServerJobs/ArchiveJob.cpp --- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -229,7 +229,8 @@ if (level_ == ResourceType_Instance) { FileInfo tmp; - if (index.LookupAttachment(tmp, id, FileContentType_Dicom)) + int64_t revision; // ignored + if (index.LookupAttachment(tmp, revision, id, FileContentType_Dicom)) { instances_.push_back(Instance(id, tmp.GetUncompressedSize())); } @@ -708,14 +709,11 @@ { private: ZipCommands& commands_; - ServerContext& context_; unsigned int counter_; public: - MediaIndexVisitor(ZipCommands& commands, - ServerContext& context) : + explicit MediaIndexVisitor(ZipCommands& commands) : commands_(commands), - context_(context), counter_(0) { } @@ -746,7 +744,6 @@ class ArchiveJob::ZipWriterIterator : public boost::noncopyable { private: - TemporaryFile& target_; ServerContext& context_; ZipCommands commands_; std::unique_ptr zip_; @@ -754,18 +751,17 @@ bool isMedia_; public: - ZipWriterIterator(TemporaryFile& target, + ZipWriterIterator(const TemporaryFile& target, ServerContext& context, ArchiveIndex& archive, bool isMedia, bool enableExtendedSopClass) : - target_(target), context_(context), isMedia_(isMedia) { if (isMedia) { - MediaIndexVisitor visitor(commands_, context); + MediaIndexVisitor visitor(commands_); archive.Expand(context.GetIndex()); commands_.AddOpenDirectory(MEDIA_IMAGES_FOLDER); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/ServerToolbox.cpp --- a/OrthancServer/Sources/ServerToolbox.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -34,7 +34,6 @@ #include "PrecompiledHeadersServer.h" #include "ServerToolbox.h" -#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h" #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" #include "../../OrthancFramework/Sources/Logging.h" @@ -77,95 +76,10 @@ }; - static void StoreMainDicomTagsInternal(ResourcesContent& target, - int64_t resource, - const DicomMap& tags) - { - DicomArray flattened(tags); - - for (size_t i = 0; i < flattened.GetSize(); i++) - { - const DicomElement& element = flattened.GetElement(i); - const DicomTag& tag = element.GetTag(); - const DicomValue& value = element.GetValue(); - if (!value.IsNull() && - !value.IsBinary()) - { - target.AddMainDicomTag(resource, tag, element.GetValue().GetContent()); - } - } - } - - - static void StoreIdentifiers(ResourcesContent& target, - int64_t resource, - ResourceType level, - const DicomMap& map) - { - const DicomTag* tags; - size_t size; - - ServerToolbox::LoadIdentifiers(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - // The identifiers tags are a subset of the main DICOM tags - assert(DicomMap::IsMainDicomTag(tags[i])); - - const DicomValue* value = map.TestAndGetValue(tags[i]); - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) - { - std::string s = ServerToolbox::NormalizeIdentifier(value->GetContent()); - target.AddIdentifierTag(resource, tags[i], s); - } - } - } - - - void ResourcesContent::AddResource(int64_t resource, - ResourceType level, - const DicomMap& dicomSummary) - { - StoreIdentifiers(*this, resource, level, dicomSummary); - - DicomMap tags; - - switch (level) - { - case ResourceType_Patient: - dicomSummary.ExtractPatientInformation(tags); - break; - - case ResourceType_Study: - // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6) - dicomSummary.ExtractPatientInformation(tags); - StoreMainDicomTagsInternal(*this, resource, tags); - - dicomSummary.ExtractStudyInformation(tags); - break; - - case ResourceType_Series: - dicomSummary.ExtractSeriesInformation(tags); - break; - - case ResourceType_Instance: - dicomSummary.ExtractInstanceInformation(tags); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - StoreMainDicomTagsInternal(*this, resource, tags); - } - - namespace ServerToolbox { bool FindOneChildInstance(int64_t& result, - IDatabaseWrapper& database, + IDatabaseWrapper::ITransaction& transaction, int64_t resource, ResourceType type) { @@ -178,7 +92,7 @@ } std::list children; - database.GetChildrenInternalId(children, resource); + transaction.GetChildrenInternalId(children, resource); if (children.empty()) { return false; @@ -190,7 +104,7 @@ } - void ReconstructMainDicomTags(IDatabaseWrapper& database, + void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction, IStorageArea& storageArea, ResourceType level) { @@ -230,7 +144,7 @@ LOG(WARNING) << "Upgrade: Reconstructing the main DICOM tags of all the " << plural << "..."; std::list resources; - database.GetAllPublicIds(resources, level); + transaction.GetAllPublicIds(resources, level); for (std::list::const_iterator it = resources.begin(); it != resources.end(); ++it) @@ -239,9 +153,9 @@ int64_t resource, instance; ResourceType tmp; - if (!database.LookupResource(resource, tmp, *it) || + if (!transaction.LookupResource(resource, tmp, *it) || tmp != level || - !FindOneChildInstance(instance, database, resource, level)) + !FindOneChildInstance(instance, transaction, resource, level)) { throw OrthancException(ErrorCode_InternalError, "Cannot find an instance for " + @@ -251,11 +165,12 @@ // Get the DICOM file attached to some instances in the resource FileInfo attachment; - if (!database.LookupAttachment(attachment, instance, FileContentType_Dicom)) + int64_t revision; + if (!transaction.LookupAttachment(attachment, revision, instance, FileContentType_Dicom)) { throw OrthancException(ErrorCode_InternalError, "Cannot retrieve the DICOM file associated with instance " + - database.GetPublicId(instance)); + transaction.GetPublicId(instance)); } try @@ -272,16 +187,16 @@ DicomMap dicomSummary; OrthancConfiguration::DefaultExtractDicomSummary(dicomSummary, dicom); - database.ClearMainDicomTags(resource); + transaction.ClearMainDicomTags(resource); - ResourcesContent tags; + ResourcesContent tags(false /* prevent the setting of metadata */); tags.AddResource(resource, level, dicomSummary); - database.SetResourcesContent(tags); + transaction.SetResourcesContent(tags); } catch (OrthancException&) { LOG(ERROR) << "Cannot decode the DICOM file with UUID " << attachment.GetUuid() - << " associated with instance " << database.GetPublicId(instance); + << " associated with instance " << transaction.GetPublicId(instance); throw; } } @@ -380,7 +295,8 @@ ServerContext::DicomCacheLocker locker(context, *it); // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext" - context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson); + context.GetIndex().DeleteAttachment( + *it, FileContentType_DicomAsJson, false /* no revision */, -1 /* dummy revision */); context.GetIndex().ReconstructInstance(locker.GetDicom()); } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/ServerToolbox.h --- a/OrthancServer/Sources/ServerToolbox.h Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/ServerToolbox.h Tue Apr 20 18:11:29 2021 +0200 @@ -33,6 +33,7 @@ #pragma once +#include "Database/IDatabaseWrapper.h" #include "ServerEnumerations.h" #include @@ -41,17 +42,16 @@ namespace Orthanc { class ServerContext; - class IDatabaseWrapper; class IStorageArea; namespace ServerToolbox { bool FindOneChildInstance(int64_t& result, - IDatabaseWrapper& database, + IDatabaseWrapper::ITransaction& transaction, int64_t resource, ResourceType type); - void ReconstructMainDicomTags(IDatabaseWrapper& database, + void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction, IStorageArea& storageArea, ResourceType level); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/SliceOrdering.cpp --- a/OrthancServer/Sources/SliceOrdering.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/SliceOrdering.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -208,7 +208,8 @@ try { - if (index.LookupMetadata(s, instanceId, ResourceType_Instance, MetadataType_Instance_IndexInSeries)) + int64_t revision; // Ignored + if (index.LookupMetadata(s, revision, instanceId, ResourceType_Instance, MetadataType_Instance_IndexInSeries)) { indexInSeries_ = boost::lexical_cast(Toolbox::StripSpaces(s)); hasIndexInSeries_ = true; diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/Sources/main.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -723,6 +723,8 @@ PrintErrorCode(ErrorCode_SslInitialization, "Cannot initialize SSL encryption, check out your certificates"); PrintErrorCode(ErrorCode_DiscontinuedAbi, "Calling a function that has been removed from the Orthanc Framework"); PrintErrorCode(ErrorCode_BadRange, "Incorrect range request"); + PrintErrorCode(ErrorCode_DatabaseCannotSerialize, "Database could not serialize access due to concurrent update, the transaction should be retried"); + PrintErrorCode(ErrorCode_Revision, "A bad revision number was provided, which might indicate conflict between multiple writers"); PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened"); PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open"); PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database"); @@ -1352,6 +1354,7 @@ { plugins_->SetServerContext(context_); context_.SetPlugins(*plugins_); + context_.GetIndex().SetMaxDatabaseRetries(plugins_->GetMaxDatabaseRetries()); } #endif } @@ -1486,6 +1489,25 @@ ": Please run Orthanc with the \"--upgrade\" argument"); } + { + static const char* const CHECK_REVISIONS = "CheckRevisions"; + + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) + { + if (database.HasRevisionsSupport()) + { + LOG(INFO) << "Handling of revisions is enabled, and the custom database back-end *has* " + << "support for revisions of metadata and attachments"; + } + else + { + LOG(WARNING) << "The custom database back-end has *no* support for revisions of metadata and attachments, " + << "but configuration option \"" << CHECK_REVISIONS << "\" is set to \"true\""; + } + } + } + bool success = ConfigureServerContext (database, storageArea, plugins, loadJobsFromDatabase); @@ -1504,7 +1526,13 @@ std::unique_ptr storage; #if ORTHANC_ENABLE_PLUGINS == 1 - OrthancPlugins plugins; + std::string databaseServerIdentifier; + { + OrthancConfiguration::ReaderLock lock; + databaseServerIdentifier = lock.GetConfiguration().GetDatabaseServerIdentifier(); + } + + OrthancPlugins plugins(databaseServerIdentifier); plugins.SetCommandLineArguments(argc, argv); LoadPlugins(plugins); diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/UnitTestsSources/ServerIndexTests.cpp --- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -75,23 +75,18 @@ ancestorType_ = type; } - virtual void SignalFileDeleted(const FileInfo& info) ORTHANC_OVERRIDE + virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE { const std::string fileUuid = info.GetUuid(); deletedFiles_.push_back(fileUuid); LOG(INFO) << "A file must be removed: " << fileUuid; } - virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE + virtual void SignalResourceDeleted(ResourceType type, + const std::string& publicId) ORTHANC_OVERRIDE { - if (change.GetChangeType() == ChangeType_Deleted) - { - deletedResources_.push_back(change.GetPublicId()); - } - - LOG(INFO) << "Change related to resource " << change.GetPublicId() << " of type " - << EnumerationToString(change.GetResourceType()) << ": " - << EnumerationToString(change.GetChangeType()); + LOG(INFO) << "Deleted resource " << publicId << " of type " << EnumerationToString(type); + deletedResources_.push_back(publicId); } }; @@ -101,6 +96,7 @@ protected: std::unique_ptr listener_; std::unique_ptr index_; + std::unique_ptr transaction_; public: DatabaseWrapperTest() @@ -111,12 +107,16 @@ { listener_.reset(new TestDatabaseListener); index_.reset(new SQLiteDatabaseWrapper); - index_->SetListener(*listener_); index_->Open(); + transaction_.reset(dynamic_cast( + index_->StartTransaction(TransactionType_ReadWrite, *listener_))); } virtual void TearDown() ORTHANC_OVERRIDE { + transaction_->Commit(0); + transaction_.reset(); + index_->Close(); index_.reset(NULL); listener_.reset(NULL); @@ -124,33 +124,33 @@ void CheckTableRecordCount(uint32_t expected, const char* table) { - ASSERT_EQ(expected, index_->GetTableRecordCount(table)); + ASSERT_EQ(expected, transaction_->GetTableRecordCount(table)); } void CheckNoParent(int64_t id) { std::string s; - ASSERT_FALSE(index_->GetParentPublicId(s, id)); + ASSERT_FALSE(transaction_->GetParentPublicId(s, id)); } void CheckParentPublicId(const char* expected, int64_t id) { std::string s; - ASSERT_TRUE(index_->GetParentPublicId(s, id)); + ASSERT_TRUE(transaction_->GetParentPublicId(s, id)); ASSERT_EQ(expected, s); } void CheckNoChild(int64_t id) { std::list j; - index_->GetChildren(j, id); + transaction_->GetChildren(j, id); ASSERT_EQ(0u, j.size()); } void CheckOneChild(const char* expected, int64_t id) { std::list j; - index_->GetChildren(j, id); + transaction_->GetChildren(j, id); ASSERT_EQ(1u, j.size()); ASSERT_EQ(expected, j.front()); } @@ -160,7 +160,7 @@ int64_t id) { std::list j; - index_->GetChildren(j, id); + transaction_->GetChildren(j, id); ASSERT_EQ(2u, j.size()); ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) || (expected1 == j.back() && expected2 == j.front())); @@ -179,7 +179,7 @@ std::vector lookup; lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); - index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */); + transaction_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */); } void DoLookupIdentifier2(std::list& result, @@ -199,7 +199,7 @@ lookup.push_back(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); - index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */); + transaction_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */); } }; } @@ -208,65 +208,65 @@ TEST_F(DatabaseWrapperTest, Simple) { int64_t a[] = { - index_->CreateResource("a", ResourceType_Patient), // 0 - index_->CreateResource("b", ResourceType_Study), // 1 - index_->CreateResource("c", ResourceType_Series), // 2 - index_->CreateResource("d", ResourceType_Instance), // 3 - index_->CreateResource("e", ResourceType_Instance), // 4 - index_->CreateResource("f", ResourceType_Instance), // 5 - index_->CreateResource("g", ResourceType_Study) // 6 + transaction_->CreateResource("a", ResourceType_Patient), // 0 + transaction_->CreateResource("b", ResourceType_Study), // 1 + transaction_->CreateResource("c", ResourceType_Series), // 2 + transaction_->CreateResource("d", ResourceType_Instance), // 3 + transaction_->CreateResource("e", ResourceType_Instance), // 4 + transaction_->CreateResource("f", ResourceType_Instance), // 5 + transaction_->CreateResource("g", ResourceType_Study) // 6 }; - ASSERT_EQ("a", index_->GetPublicId(a[0])); - ASSERT_EQ("b", index_->GetPublicId(a[1])); - ASSERT_EQ("c", index_->GetPublicId(a[2])); - ASSERT_EQ("d", index_->GetPublicId(a[3])); - ASSERT_EQ("e", index_->GetPublicId(a[4])); - ASSERT_EQ("f", index_->GetPublicId(a[5])); - ASSERT_EQ("g", index_->GetPublicId(a[6])); + ASSERT_EQ("a", transaction_->GetPublicId(a[0])); + ASSERT_EQ("b", transaction_->GetPublicId(a[1])); + ASSERT_EQ("c", transaction_->GetPublicId(a[2])); + ASSERT_EQ("d", transaction_->GetPublicId(a[3])); + ASSERT_EQ("e", transaction_->GetPublicId(a[4])); + ASSERT_EQ("f", transaction_->GetPublicId(a[5])); + ASSERT_EQ("g", transaction_->GetPublicId(a[6])); - ASSERT_EQ(ResourceType_Patient, index_->GetResourceType(a[0])); - ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[1])); - ASSERT_EQ(ResourceType_Series, index_->GetResourceType(a[2])); - ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[3])); - ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[4])); - ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[5])); - ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[6])); + ASSERT_EQ(ResourceType_Patient, transaction_->GetResourceType(a[0])); + ASSERT_EQ(ResourceType_Study, transaction_->GetResourceType(a[1])); + ASSERT_EQ(ResourceType_Series, transaction_->GetResourceType(a[2])); + ASSERT_EQ(ResourceType_Instance, transaction_->GetResourceType(a[3])); + ASSERT_EQ(ResourceType_Instance, transaction_->GetResourceType(a[4])); + ASSERT_EQ(ResourceType_Instance, transaction_->GetResourceType(a[5])); + ASSERT_EQ(ResourceType_Study, transaction_->GetResourceType(a[6])); { std::list t; - index_->GetAllPublicIds(t, ResourceType_Patient); + transaction_->GetAllPublicIds(t, ResourceType_Patient); ASSERT_EQ(1u, t.size()); ASSERT_EQ("a", t.front()); - index_->GetAllPublicIds(t, ResourceType_Series); + transaction_->GetAllPublicIds(t, ResourceType_Series); ASSERT_EQ(1u, t.size()); ASSERT_EQ("c", t.front()); - index_->GetAllPublicIds(t, ResourceType_Study); + transaction_->GetAllPublicIds(t, ResourceType_Study); ASSERT_EQ(2u, t.size()); - index_->GetAllPublicIds(t, ResourceType_Instance); + transaction_->GetAllPublicIds(t, ResourceType_Instance); ASSERT_EQ(3u, t.size()); } - index_->SetGlobalProperty(GlobalProperty_FlushSleep, "World"); + transaction_->SetGlobalProperty(GlobalProperty_FlushSleep, true, "World"); - index_->AttachChild(a[0], a[1]); - index_->AttachChild(a[1], a[2]); - index_->AttachChild(a[2], a[3]); - index_->AttachChild(a[2], a[4]); - index_->AttachChild(a[6], a[5]); + transaction_->AttachChild(a[0], a[1]); + transaction_->AttachChild(a[1], a[2]); + transaction_->AttachChild(a[2], a[3]); + transaction_->AttachChild(a[2], a[4]); + transaction_->AttachChild(a[6], a[5]); int64_t parent; - ASSERT_FALSE(index_->LookupParent(parent, a[0])); - ASSERT_TRUE(index_->LookupParent(parent, a[1])); ASSERT_EQ(a[0], parent); - ASSERT_TRUE(index_->LookupParent(parent, a[2])); ASSERT_EQ(a[1], parent); - ASSERT_TRUE(index_->LookupParent(parent, a[3])); ASSERT_EQ(a[2], parent); - ASSERT_TRUE(index_->LookupParent(parent, a[4])); ASSERT_EQ(a[2], parent); - ASSERT_TRUE(index_->LookupParent(parent, a[5])); ASSERT_EQ(a[6], parent); - ASSERT_FALSE(index_->LookupParent(parent, a[6])); + ASSERT_FALSE(transaction_->LookupParent(parent, a[0])); + ASSERT_TRUE(transaction_->LookupParent(parent, a[1])); ASSERT_EQ(a[0], parent); + ASSERT_TRUE(transaction_->LookupParent(parent, a[2])); ASSERT_EQ(a[1], parent); + ASSERT_TRUE(transaction_->LookupParent(parent, a[3])); ASSERT_EQ(a[2], parent); + ASSERT_TRUE(transaction_->LookupParent(parent, a[4])); ASSERT_EQ(a[2], parent); + ASSERT_TRUE(transaction_->LookupParent(parent, a[5])); ASSERT_EQ(a[6], parent); + ASSERT_FALSE(transaction_->LookupParent(parent, a[6])); std::string s; @@ -279,14 +279,14 @@ CheckParentPublicId("g", a[5]); std::list l; - index_->GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front()); - index_->GetChildrenPublicId(l, a[1]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("c", l.front()); - index_->GetChildrenPublicId(l, a[3]); ASSERT_EQ(0u, l.size()); - index_->GetChildrenPublicId(l, a[4]); ASSERT_EQ(0u, l.size()); - index_->GetChildrenPublicId(l, a[5]); ASSERT_EQ(0u, l.size()); - index_->GetChildrenPublicId(l, a[6]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("f", l.front()); + transaction_->GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front()); + transaction_->GetChildrenPublicId(l, a[1]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("c", l.front()); + transaction_->GetChildrenPublicId(l, a[3]); ASSERT_EQ(0u, l.size()); + transaction_->GetChildrenPublicId(l, a[4]); ASSERT_EQ(0u, l.size()); + transaction_->GetChildrenPublicId(l, a[5]); ASSERT_EQ(0u, l.size()); + transaction_->GetChildrenPublicId(l, a[6]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("f", l.front()); - index_->GetChildrenPublicId(l, a[2]); ASSERT_EQ(2u, l.size()); + transaction_->GetChildrenPublicId(l, a[2]); ASSERT_EQ(2u, l.size()); if (l.front() == "d") { ASSERT_EQ("e", l.back()); @@ -298,64 +298,72 @@ } std::map md; - index_->GetAllMetadata(md, a[4]); + transaction_->GetAllMetadata(md, a[4]); ASSERT_EQ(0u, md.size()); - index_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", - CompressionType_ZlibWithSize, 21, "compressedMD5")); - index_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5")); - index_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5")); - index_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE"); + transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", + CompressionType_ZlibWithSize, 21, "compressedMD5"), 42); + transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"), 43); + transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"), 44); - index_->GetAllMetadata(md, a[4]); + // TODO - REVISIONS - "42" is revision number, that is not currently stored (*) + transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42); + + transaction_->GetAllMetadata(md, a[4]); ASSERT_EQ(1u, md.size()); ASSERT_EQ("PINNACLE", md[MetadataType_RemoteAet]); - index_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU"); - index_->GetAllMetadata(md, a[4]); + transaction_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU", 10); + transaction_->GetAllMetadata(md, a[4]); ASSERT_EQ(2u, md.size()); std::map md2; - index_->GetAllMetadata(md2, a[4]); + transaction_->GetAllMetadata(md2, a[4]); ASSERT_EQ(2u, md2.size()); ASSERT_EQ("TUTU", md2[MetadataType_ModifiedFrom]); ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]); - index_->DeleteMetadata(a[4], MetadataType_ModifiedFrom); - index_->GetAllMetadata(md, a[4]); + transaction_->DeleteMetadata(a[4], MetadataType_ModifiedFrom); + transaction_->GetAllMetadata(md, a[4]); ASSERT_EQ(1u, md.size()); ASSERT_EQ("PINNACLE", md[MetadataType_RemoteAet]); - index_->GetAllMetadata(md2, a[4]); + transaction_->GetAllMetadata(md2, a[4]); ASSERT_EQ(1u, md2.size()); ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]); - ASSERT_EQ(21u + 42u + 44u, index_->GetTotalCompressedSize()); - ASSERT_EQ(42u + 42u + 44u, index_->GetTotalUncompressedSize()); + ASSERT_EQ(21u + 42u + 44u, transaction_->GetTotalCompressedSize()); + ASSERT_EQ(42u + 42u + 44u, transaction_->GetTotalUncompressedSize()); - index_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName"); + transaction_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName"); int64_t b; ResourceType t; - ASSERT_TRUE(index_->LookupResource(b, t, "g")); + ASSERT_TRUE(transaction_->LookupResource(b, t, "g")); ASSERT_EQ(7, b); ASSERT_EQ(ResourceType_Study, t); - ASSERT_TRUE(index_->LookupMetadata(s, a[4], MetadataType_RemoteAet)); - ASSERT_FALSE(index_->LookupMetadata(s, a[4], MetadataType_Instance_IndexInSeries)); + int64_t revision; + ASSERT_TRUE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_RemoteAet)); + ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_FALSE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_Instance_IndexInSeries)); + ASSERT_EQ(0, revision); ASSERT_EQ("PINNACLE", s); std::string u; - ASSERT_TRUE(index_->LookupMetadata(u, a[4], MetadataType_RemoteAet)); + ASSERT_TRUE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_RemoteAet)); + ASSERT_EQ(0, revision); ASSERT_EQ("PINNACLE", u); - ASSERT_FALSE(index_->LookupMetadata(u, a[4], MetadataType_Instance_IndexInSeries)); + ASSERT_FALSE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_Instance_IndexInSeries)); + ASSERT_EQ(0, revision); - ASSERT_TRUE(index_->LookupGlobalProperty(s, GlobalProperty_FlushSleep)); - ASSERT_FALSE(index_->LookupGlobalProperty(s, static_cast(42))); + ASSERT_TRUE(transaction_->LookupGlobalProperty(s, GlobalProperty_FlushSleep, true)); + ASSERT_FALSE(transaction_->LookupGlobalProperty(s, static_cast(42), true)); ASSERT_EQ("World", s); FileInfo att; - ASSERT_TRUE(index_->LookupAttachment(att, a[4], FileContentType_DicomAsJson)); + ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson)); + ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) ASSERT_EQ("my json file", att.GetUuid()); ASSERT_EQ(21u, att.GetCompressedSize()); ASSERT_EQ("md5", att.GetUncompressedMD5()); @@ -363,7 +371,8 @@ ASSERT_EQ(42u, att.GetUncompressedSize()); ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType()); - ASSERT_TRUE(index_->LookupAttachment(att, a[6], FileContentType_Dicom)); + ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom)); + ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) ASSERT_EQ("world", att.GetUuid()); ASSERT_EQ(44u, att.GetCompressedSize()); ASSERT_EQ("md5", att.GetUncompressedMD5()); @@ -379,7 +388,7 @@ CheckTableRecordCount(1, "Metadata"); CheckTableRecordCount(1, "MainDicomTags"); - index_->DeleteResource(a[0]); + transaction_->DeleteResource(a[0]); ASSERT_EQ(5u, listener_->deletedResources_.size()); ASSERT_EQ(2u, listener_->deletedFiles_.size()); ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), @@ -394,7 +403,7 @@ CheckTableRecordCount(1, "AttachedFiles"); CheckTableRecordCount(0, "MainDicomTags"); - index_->DeleteResource(a[5]); + transaction_->DeleteResource(a[5]); ASSERT_EQ(7u, listener_->deletedResources_.size()); CheckTableRecordCount(0, "Resources"); @@ -402,11 +411,11 @@ CheckTableRecordCount(3, "GlobalProperties"); std::string tmp; - ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion)); + ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true)); ASSERT_EQ("6", tmp); - ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep)); + ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep, true)); ASSERT_EQ("World", tmp); - ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast)); + ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true)); ASSERT_EQ("1", tmp); ASSERT_EQ(3u, listener_->deletedFiles_.size()); @@ -419,23 +428,23 @@ TEST_F(DatabaseWrapperTest, Upward) { int64_t a[] = { - index_->CreateResource("a", ResourceType_Patient), // 0 - index_->CreateResource("b", ResourceType_Study), // 1 - index_->CreateResource("c", ResourceType_Series), // 2 - index_->CreateResource("d", ResourceType_Instance), // 3 - index_->CreateResource("e", ResourceType_Instance), // 4 - index_->CreateResource("f", ResourceType_Study), // 5 - index_->CreateResource("g", ResourceType_Series), // 6 - index_->CreateResource("h", ResourceType_Series) // 7 + transaction_->CreateResource("a", ResourceType_Patient), // 0 + transaction_->CreateResource("b", ResourceType_Study), // 1 + transaction_->CreateResource("c", ResourceType_Series), // 2 + transaction_->CreateResource("d", ResourceType_Instance), // 3 + transaction_->CreateResource("e", ResourceType_Instance), // 4 + transaction_->CreateResource("f", ResourceType_Study), // 5 + transaction_->CreateResource("g", ResourceType_Series), // 6 + transaction_->CreateResource("h", ResourceType_Series) // 7 }; - index_->AttachChild(a[0], a[1]); - index_->AttachChild(a[1], a[2]); - index_->AttachChild(a[2], a[3]); - index_->AttachChild(a[2], a[4]); - index_->AttachChild(a[1], a[6]); - index_->AttachChild(a[0], a[5]); - index_->AttachChild(a[5], a[7]); + transaction_->AttachChild(a[0], a[1]); + transaction_->AttachChild(a[1], a[2]); + transaction_->AttachChild(a[2], a[3]); + transaction_->AttachChild(a[2], a[4]); + transaction_->AttachChild(a[1], a[6]); + transaction_->AttachChild(a[0], a[5]); + transaction_->AttachChild(a[5], a[7]); CheckTwoChildren("b", "f", a[0]); CheckTwoChildren("c", "g", a[1]); @@ -447,22 +456,22 @@ CheckNoChild(a[7]); listener_->Reset(); - index_->DeleteResource(a[3]); + transaction_->DeleteResource(a[3]); ASSERT_EQ("c", listener_->ancestorId_); ASSERT_EQ(ResourceType_Series, listener_->ancestorType_); listener_->Reset(); - index_->DeleteResource(a[4]); + transaction_->DeleteResource(a[4]); ASSERT_EQ("b", listener_->ancestorId_); ASSERT_EQ(ResourceType_Study, listener_->ancestorType_); listener_->Reset(); - index_->DeleteResource(a[7]); + transaction_->DeleteResource(a[7]); ASSERT_EQ("a", listener_->ancestorId_); ASSERT_EQ(ResourceType_Patient, listener_->ancestorType_); listener_->Reset(); - index_->DeleteResource(a[6]); + transaction_->DeleteResource(a[6]); ASSERT_EQ("", listener_->ancestorId_); // No more ancestor } @@ -473,10 +482,10 @@ for (int i = 0; i < 10; i++) { std::string p = "Patient " + boost::lexical_cast(i); - patients.push_back(index_->CreateResource(p, ResourceType_Patient)); - index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast(i))); - ASSERT_FALSE(index_->IsProtectedPatient(patients[i])); + patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); + transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, + "md5-" + boost::lexical_cast(i)), 42); + ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } CheckTableRecordCount(10u, "Resources"); @@ -485,8 +494,8 @@ listener_->Reset(); ASSERT_EQ(0u, listener_->deletedResources_.size()); - index_->DeleteResource(patients[5]); - index_->DeleteResource(patients[0]); + transaction_->DeleteResource(patients[5]); + transaction_->DeleteResource(patients[0]); ASSERT_EQ(2u, listener_->deletedResources_.size()); CheckTableRecordCount(8u, "Resources"); @@ -497,28 +506,28 @@ ASSERT_EQ("Patient 0", listener_->deletedFiles_[1]); int64_t p; - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); - index_->DeleteResource(p); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); + transaction_->DeleteResource(p); ASSERT_EQ(3u, listener_->deletedResources_.size()); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); - index_->DeleteResource(p); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); + transaction_->DeleteResource(p); ASSERT_EQ(4u, listener_->deletedResources_.size()); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); - index_->DeleteResource(p); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); + transaction_->DeleteResource(p); ASSERT_EQ(5u, listener_->deletedResources_.size()); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); - index_->DeleteResource(p); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); + transaction_->DeleteResource(p); ASSERT_EQ(6u, listener_->deletedResources_.size()); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]); - index_->DeleteResource(p); - index_->DeleteResource(patients[8]); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]); + transaction_->DeleteResource(p); + transaction_->DeleteResource(patients[8]); ASSERT_EQ(8u, listener_->deletedResources_.size()); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]); - index_->DeleteResource(p); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]); + transaction_->DeleteResource(p); ASSERT_EQ(9u, listener_->deletedResources_.size()); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]); - index_->DeleteResource(p); - ASSERT_FALSE(index_->SelectPatientToRecycle(p)); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]); + transaction_->DeleteResource(p); + ASSERT_FALSE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(10u, listener_->deletedResources_.size()); ASSERT_EQ(10u, listener_->deletedFiles_.size()); @@ -534,39 +543,39 @@ for (int i = 0; i < 5; i++) { std::string p = "Patient " + boost::lexical_cast(i); - patients.push_back(index_->CreateResource(p, ResourceType_Patient)); - index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast(i))); - ASSERT_FALSE(index_->IsProtectedPatient(patients[i])); + patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); + transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, + "md5-" + boost::lexical_cast(i)), 42); + ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } CheckTableRecordCount(5, "Resources"); CheckTableRecordCount(5, "PatientRecyclingOrder"); - ASSERT_FALSE(index_->IsProtectedPatient(patients[2])); - index_->SetProtectedPatient(patients[2], true); - ASSERT_TRUE(index_->IsProtectedPatient(patients[2])); + ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2])); + transaction_->SetProtectedPatient(patients[2], true); + ASSERT_TRUE(transaction_->IsProtectedPatient(patients[2])); CheckTableRecordCount(5, "Resources"); CheckTableRecordCount(4, "PatientRecyclingOrder"); - index_->SetProtectedPatient(patients[2], true); - ASSERT_TRUE(index_->IsProtectedPatient(patients[2])); + transaction_->SetProtectedPatient(patients[2], true); + ASSERT_TRUE(transaction_->IsProtectedPatient(patients[2])); CheckTableRecordCount(4, "PatientRecyclingOrder"); - index_->SetProtectedPatient(patients[2], false); - ASSERT_FALSE(index_->IsProtectedPatient(patients[2])); + transaction_->SetProtectedPatient(patients[2], false); + ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2])); CheckTableRecordCount(5, "PatientRecyclingOrder"); - index_->SetProtectedPatient(patients[2], false); - ASSERT_FALSE(index_->IsProtectedPatient(patients[2])); + transaction_->SetProtectedPatient(patients[2], false); + ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2])); CheckTableRecordCount(5, "PatientRecyclingOrder"); CheckTableRecordCount(5, "Resources"); - index_->SetProtectedPatient(patients[2], true); - ASSERT_TRUE(index_->IsProtectedPatient(patients[2])); + transaction_->SetProtectedPatient(patients[2], true); + ASSERT_TRUE(transaction_->IsProtectedPatient(patients[2])); CheckTableRecordCount(4, "PatientRecyclingOrder"); - index_->SetProtectedPatient(patients[2], false); - ASSERT_FALSE(index_->IsProtectedPatient(patients[2])); + transaction_->SetProtectedPatient(patients[2], false); + ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2])); CheckTableRecordCount(5, "PatientRecyclingOrder"); - index_->SetProtectedPatient(patients[3], true); - ASSERT_TRUE(index_->IsProtectedPatient(patients[3])); + transaction_->SetProtectedPatient(patients[3], true); + ASSERT_TRUE(transaction_->IsProtectedPatient(patients[3])); CheckTableRecordCount(4, "PatientRecyclingOrder"); CheckTableRecordCount(5, "Resources"); @@ -575,33 +584,33 @@ // Unprotecting a patient puts it at the last position in the recycling queue int64_t p; ASSERT_EQ(0u, listener_->deletedResources_.size()); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]); - index_->DeleteResource(p); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]); + transaction_->DeleteResource(p); ASSERT_EQ(1u, listener_->deletedResources_.size()); - ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); - index_->DeleteResource(p); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); + transaction_->DeleteResource(p); ASSERT_EQ(2u, listener_->deletedResources_.size()); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); - index_->DeleteResource(p); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); + transaction_->DeleteResource(p); ASSERT_EQ(3u, listener_->deletedResources_.size()); - ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[2])); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); - index_->DeleteResource(p); + ASSERT_FALSE(transaction_->SelectPatientToRecycle(p, patients[2])); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); + transaction_->DeleteResource(p); ASSERT_EQ(4u, listener_->deletedResources_.size()); // "patients[3]" is still protected - ASSERT_FALSE(index_->SelectPatientToRecycle(p)); + ASSERT_FALSE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(4u, listener_->deletedFiles_.size()); CheckTableRecordCount(1, "Resources"); CheckTableRecordCount(0, "PatientRecyclingOrder"); - index_->SetProtectedPatient(patients[3], false); + transaction_->SetProtectedPatient(patients[3], false); CheckTableRecordCount(1, "PatientRecyclingOrder"); - ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[3])); - ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[2])); - ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); - index_->DeleteResource(p); + ASSERT_FALSE(transaction_->SelectPatientToRecycle(p, patients[3])); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p, patients[2])); + ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); + transaction_->DeleteResource(p); ASSERT_EQ(5u, listener_->deletedResources_.size()); ASSERT_EQ(5u, listener_->deletedFiles_.size()); @@ -623,10 +632,10 @@ ServerIndex& index = context.GetIndex(); - ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); - ASSERT_EQ(2u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); - ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); - ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); + ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true)); + ASSERT_EQ(2u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true)); + ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true)); + ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true)); context.Stop(); db.Close(); @@ -636,16 +645,16 @@ TEST_F(DatabaseWrapperTest, LookupIdentifier) { int64_t a[] = { - index_->CreateResource("a", ResourceType_Study), // 0 - index_->CreateResource("b", ResourceType_Study), // 1 - index_->CreateResource("c", ResourceType_Study), // 2 - index_->CreateResource("d", ResourceType_Series) // 3 + transaction_->CreateResource("a", ResourceType_Study), // 0 + transaction_->CreateResource("b", ResourceType_Study), // 1 + transaction_->CreateResource("c", ResourceType_Study), // 2 + transaction_->CreateResource("d", ResourceType_Series) // 3 }; - index_->SetIdentifierTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0"); - index_->SetIdentifierTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1"); - index_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0"); - index_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0"); + transaction_->SetIdentifierTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0"); + transaction_->SetIdentifierTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1"); + transaction_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0"); + transaction_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0"); std::list s; @@ -776,7 +785,9 @@ for (size_t i = 0; i < ids.size(); i++) { FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5"); - index.AddAttachment(info, ids[i]); + int64_t revision = -1; + index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1); + ASSERT_EQ(0, revision); index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances); @@ -854,12 +865,17 @@ { FileInfo nope; - ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson)); + int64_t revision; + ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson)); } FileInfo dicom1, pixelData1; - ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, id, FileContentType_Dicom)); - ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, id, FileContentType_DicomUntilPixelData)); + int64_t revision; + ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, revision, id, FileContentType_Dicom)); + ASSERT_EQ(0, revision); + revision = -1; + ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, revision, id, FileContentType_DicomUntilPixelData)); + ASSERT_EQ(0, revision); context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances); @@ -899,12 +915,16 @@ { FileInfo nope; - ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson)); + int64_t revision; + ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson)); } FileInfo dicom2, pixelData2; - ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, id, FileContentType_Dicom)); - ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, id, FileContentType_DicomUntilPixelData)); + ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, revision, id, FileContentType_Dicom)); + ASSERT_EQ(0, revision); + revision = -1; + ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, revision, id, FileContentType_DicomUntilPixelData)); + ASSERT_EQ(0, revision); context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances); @@ -1008,12 +1028,14 @@ } std::string s; - bool found = context.GetIndex().LookupMetadata(s, id, ResourceType_Instance, + int64_t revision; + bool found = context.GetIndex().LookupMetadata(s, revision, id, ResourceType_Instance, MetadataType_Instance_PixelDataOffset); if (withPixelData) { ASSERT_TRUE(found); + ASSERT_EQ(0, revision); ASSERT_GT(boost::lexical_cast(s), 128 /* length of the DICOM preamble */); ASSERT_LT(boost::lexical_cast(s), dicomSize); } diff -r 844ec5ecb6ef -r ee8706477b61 OrthancServer/UnitTestsSources/SizeOfTests.cpp --- a/OrthancServer/UnitTestsSources/SizeOfTests.cpp Mon Apr 19 10:28:24 2021 +0200 +++ b/OrthancServer/UnitTestsSources/SizeOfTests.cpp Tue Apr 20 18:11:29 2021 +0200 @@ -129,7 +129,6 @@ #include "../../OrthancFramework/Sources/Images/ImageBuffer.h" #include "../../OrthancFramework/Sources/Images/ImageProcessing.h" #include "../../OrthancFramework/Sources/Images/ImageTraits.h" -#include "../../OrthancFramework/Sources/Images/JpegErrorManager.h" #include "../../OrthancFramework/Sources/Images/JpegReader.h" #include "../../OrthancFramework/Sources/Images/JpegWriter.h" #include "../../OrthancFramework/Sources/Images/PamReader.h" diff -r 844ec5ecb6ef -r ee8706477b61 TODO --- a/TODO Mon Apr 19 10:28:24 2021 +0200 +++ b/TODO Tue Apr 20 18:11:29 2021 +0200 @@ -97,6 +97,13 @@ * Check out rapidjson: https://github.com/miloyip/nativejson-benchmark +======== +Database +======== + +* Integration test searching for "\" and "%" in PatientName, PatientID... + + ======= Plugins =======