# HG changeset patch # User Sebastien Jodogne # Date 1545221448 -3600 # Node ID 19ebb606910d8e99cf351752fccf4dbf11120258 # Parent 18a2d196414b8d7c31c896ddd55c5f5b5c705842# Parent ff65c925f57a78b4d6a547bfe76a7d1bc390460f integration mainline->db-changes diff -r ff65c925f57a -r 19ebb606910d CMakeLists.txt --- a/CMakeLists.txt Tue Dec 18 14:37:53 2018 +0100 +++ b/CMakeLists.txt Wed Dec 19 13:10:48 2018 +0100 @@ -53,7 +53,7 @@ ##################################################################### set(ORTHANC_SERVER_SOURCES - OrthancServer/DatabaseWrapper.cpp + OrthancServer/SQLiteDatabaseWrapper.cpp OrthancServer/DicomInstanceOrigin.cpp OrthancServer/DicomInstanceToStore.cpp OrthancServer/ExportedResource.cpp @@ -71,6 +71,7 @@ OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/QueryRetrieveHandler.cpp + OrthancServer/Search/DatabaseConstraint.cpp OrthancServer/Search/DatabaseLookup.cpp OrthancServer/Search/DicomTagConstraint.cpp OrthancServer/Search/HierarchicalMatcher.cpp @@ -168,13 +169,14 @@ ##################################################################### set(ORTHANC_EMBEDDED_FILES - PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql - UPGRADE_DATABASE_4_TO_5 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql - CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json - DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt - LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua - FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql + CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/InstallTrackAttachmentsSize.sql ) if (STANDALONE_BUILD) diff -r ff65c925f57a -r 19ebb606910d Core/Enumerations.cpp --- a/Core/Enumerations.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/Core/Enumerations.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -1897,6 +1897,35 @@ } + bool IsResourceLevelAboveOrEqual(ResourceType level, + ResourceType reference) + { + switch (reference) + { + case ResourceType_Patient: + return (level == ResourceType_Patient); + + case ResourceType_Study: + return (level == ResourceType_Patient || + level == ResourceType_Study); + + case ResourceType_Series: + return (level == ResourceType_Patient || + level == ResourceType_Study || + level == ResourceType_Series); + + case ResourceType_Instance: + return (level == ResourceType_Patient || + level == ResourceType_Study || + level == ResourceType_Series || + level == ResourceType_Instance); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + DicomModule GetModule(ResourceType type) { switch (type) diff -r ff65c925f57a -r 19ebb606910d Core/Enumerations.h --- a/Core/Enumerations.h Tue Dec 18 14:37:53 2018 +0100 +++ b/Core/Enumerations.h Wed Dec 19 13:10:48 2018 +0100 @@ -758,6 +758,9 @@ ResourceType GetParentResourceType(ResourceType type); + bool IsResourceLevelAboveOrEqual(ResourceType level, + ResourceType reference); + DicomModule GetModule(ResourceType type); const char* GetDicomSpecificCharacterSet(Encoding encoding); diff -r ff65c925f57a -r 19ebb606910d OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Tue Dec 18 14:37:53 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1131 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 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 "DatabaseWrapper.h" - -#include "../Core/DicomFormat/DicomArray.h" -#include "../Core/Logging.h" -#include "EmbeddedResources.h" -#include "ServerToolbox.h" - -#include -#include - -namespace Orthanc -{ - namespace Internals - { - class SignalFileDeleted : public SQLite::IScalarFunction - { - private: - IDatabaseListener& listener_; - - public: - SignalFileDeleted(IDatabaseListener& listener) : - listener_(listener) - { - } - - virtual const char* GetName() const - { - return "SignalFileDeleted"; - } - - virtual unsigned int GetCardinality() const - { - return 7; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - std::string uncompressedMD5, compressedMD5; - - if (!context.IsNullValue(5)) - { - uncompressedMD5 = context.GetStringValue(5); - } - - if (!context.IsNullValue(6)) - { - compressedMD5 = context.GetStringValue(6); - } - - FileInfo info(context.GetStringValue(0), - static_cast(context.GetIntValue(1)), - static_cast(context.GetInt64Value(2)), - uncompressedMD5, - 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); - } - }; - - 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"; - } - - virtual unsigned int GetCardinality() const - { - return 2; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - VLOG(1) << "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_; - } - }; - } - - - void DatabaseWrapper::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 DatabaseWrapper::GetExportedResourcesInternal(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); - 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 DatabaseWrapper::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)); - } - } - - - void DatabaseWrapper::DeleteResource(int64_t id) - { - 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 DatabaseWrapper::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()) - { - target = s.ColumnString(0); - return true; - } - else - { - return false; - } - } - - - int64_t DatabaseWrapper::GetTableRecordCount(const std::string& table) - { - char buf[128]; - sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str()); - SQLite::Statement s(db_, buf); - - if (!s.Step()) - { - throw OrthancException(ErrorCode_InternalError); - } - - int64_t c = s.ColumnInt(0); - assert(!s.Step()); - - return c; - } - - - DatabaseWrapper::DatabaseWrapper(const std::string& path) : - listener_(NULL), - signalRemainingAncestor_(NULL), - version_(0) - { - db_.Open(path); - } - - - DatabaseWrapper::DatabaseWrapper() : - listener_(NULL), - signalRemainingAncestor_(NULL), - version_(0) - { - db_.OpenInMemory(); - } - - - void DatabaseWrapper::Open() - { - 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"); - - if (!db_.DoesTableExist("GlobalProperties")) - { - LOG(INFO) << "Creating the database"; - std::string query; - EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE); - db_.Execute(query); - } - - // Check the version of the database - std::string tmp; - if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion)) - { - tmp = "Unknown"; - } - - bool ok = false; - try - { - LOG(INFO) << "Version of the Orthanc database: " << tmp; - version_ = boost::lexical_cast(tmp); - ok = true; - } - catch (boost::bad_lexical_cast&) - { - } - - if (!ok) - { - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, - "Incompatible version of the Orthanc database: " + tmp); - } - - signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; - db_.Register(signalRemainingAncestor_); - } - - - static void ExecuteUpgradeScript(SQLite::Connection& db, - EmbeddedResources::FileResourceId script) - { - std::string upgrade; - EmbeddedResources::GetFileResource(upgrade, script); - db.BeginTransaction(); - db.Execute(upgrade); - db.CommitTransaction(); - } - - - void DatabaseWrapper::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) - { - if (targetVersion != 6) - { - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); - } - - // This version of Orthanc is only compatible with versions 3, 4, - // 5 and 6 of the DB schema - if (version_ != 3 && - version_ != 4 && - version_ != 5 && - version_ != 6) - { - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); - } - - if (version_ == 3) - { - LOG(WARNING) << "Upgrading database version from 3 to 4"; - ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_3_TO_4); - version_ = 4; - } - - if (version_ == 4) - { - LOG(WARNING) << "Upgrading database version from 4 to 5"; - ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5); - version_ = 5; - } - - if (version_ == 5) - { - LOG(WARNING) << "Upgrading database version from 5 to 6"; - // 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(); - version_ = 6; - } - } - - - void DatabaseWrapper::SetListener(IDatabaseListener& listener) - { - listener_ = &listener; - db_.Register(new Internals::SignalFileDeleted(listener)); - db_.Register(new Internals::SignalResourceDeleted(listener)); - } - - - void DatabaseWrapper::ClearTable(const std::string& tableName) - { - db_.Execute("DELETE FROM " + tableName); - } - - - bool DatabaseWrapper::LookupParent(int64_t& parentId, - int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT parentId FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (!s.Step()) - { - throw OrthancException(ErrorCode_UnknownResource); - } - - if (s.ColumnIsNull(0)) - { - return false; - } - else - { - parentId = s.ColumnInt(0); - return true; - } - } - - - ResourceType DatabaseWrapper::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 DatabaseWrapper::GetPublicId(int64_t resourceId) - { - 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 DatabaseWrapper::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 DatabaseWrapper::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); - } - - - void DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::CreateResource(const std::string& publicId, - ResourceType type) - { - 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(); - } - - - bool DatabaseWrapper::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 DatabaseWrapper::AttachChild(int64_t parent, - int64_t child) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); - s.BindInt64(0, parent); - s.BindInt64(1, child); - s.Run(); - } - - - void DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::ListAvailableMetadata(std::list& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - target.push_back(static_cast(s.ColumnInt(0))); - } - } - - - void DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::ListAvailableAttachments(std::list& 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.push_back(static_cast(s.ColumnInt(0))); - } - } - - bool DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::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); - s.BindInt(1, tag.GetGroup()); - s.BindInt(2, tag.GetElement()); - s.BindString(3, value); - s.Run(); - } - - - void DatabaseWrapper::GetMainDicomTags(DicomMap& map, - int64_t id) - { - 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 DatabaseWrapper::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 = ?"); - s.BindInt64(0, id); - - target.clear(); - - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - void DatabaseWrapper::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 DatabaseWrapper::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.Run(); - } - - - void DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::GetTotalCompressedSize() - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); - s.Run(); - return static_cast(s.ColumnInt64(0)); - } - - - uint64_t DatabaseWrapper::GetTotalUncompressedSize() - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); - s.Run(); - return static_cast(s.ColumnInt64(0)); - } - - - uint64_t DatabaseWrapper::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 - { - int64_t c = s.ColumnInt(0); - assert(!s.Step()); - return c; - } - } - - - void DatabaseWrapper::GetAllInternalIds(std::list& target, - ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnInt64(0)); - } - } - - - void DatabaseWrapper::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 DatabaseWrapper::GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) - { - if (limit == 0) - { - target.clear(); - return; - } - - 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 DatabaseWrapper::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 DatabaseWrapper::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 DatabaseWrapper::IsProtectedPatient(int64_t internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); - s.BindInt64(0, internalId); - return !s.Step(); - } - - - void DatabaseWrapper::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 DatabaseWrapper::IsExistingResource(int64_t internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM Resources WHERE internalId=?"); - s.BindInt64(0, internalId); - return s.Step(); - } - - - void DatabaseWrapper::LookupIdentifier(std::list& target, - ResourceType level, - const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value) - { - static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " - "d.id = r.internalId AND r.resourceType=? AND " - "d.tagGroup=? AND d.tagElement=? AND "); - - std::auto_ptr s; - - switch (type) - { - case IdentifierConstraintType_GreaterOrEqual: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?")); - break; - - case IdentifierConstraintType_SmallerOrEqual: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?")); - break; - - case IdentifierConstraintType_Wildcard: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?")); - break; - - case IdentifierConstraintType_Equal: - default: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?")); - break; - } - - assert(s.get() != NULL); - - s->BindInt(0, level); - s->BindInt(1, tag.GetGroup()); - s->BindInt(2, tag.GetElement()); - s->BindString(3, value); - - target.clear(); - - while (s->Step()) - { - target.push_back(s->ColumnInt64(0)); - } - } - - - void DatabaseWrapper::LookupIdentifierRange(std::list& target, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end) - { - SQLite::Statement statement(db_, SQLITE_FROM_HERE, - "SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " - "d.id = r.internalId AND r.resourceType=? AND " - "d.tagGroup=? AND d.tagElement=? AND d.value>=? AND d.value<=?"); - - statement.BindInt(0, level); - statement.BindInt(1, tag.GetGroup()); - statement.BindInt(2, tag.GetElement()); - statement.BindString(3, start); - statement.BindString(4, end); - - target.clear(); - - while (statement.Step()) - { - target.push_back(statement.ColumnInt64(0)); - } - } -} diff -r ff65c925f57a -r 19ebb606910d OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Tue Dec 18 14:37:53 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,280 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2018 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 "IDatabaseWrapper.h" - -#include "../Core/SQLite/Connection.h" -#include "../Core/SQLite/Transaction.h" - -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 DatabaseWrapper : public IDatabaseWrapper - { - private: - IDatabaseListener* listener_; - SQLite::Connection db_; - Internals::SignalRemainingAncestor* signalRemainingAncestor_; - unsigned int version_; - - void GetChangesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults); - - void GetExportedResourcesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults); - - void ClearTable(const std::string& tableName); - - public: - DatabaseWrapper(const std::string& path); - - DatabaseWrapper(); - - virtual void Open(); - - virtual void Close() - { - db_.Close(); - } - - virtual void SetListener(IDatabaseListener& listener); - - virtual bool LookupParent(int64_t& parentId, - int64_t resourceId); - - virtual std::string GetPublicId(int64_t resourceId); - - virtual ResourceType GetResourceType(int64_t resourceId); - - virtual void DeleteResource(int64_t id); - - virtual void GetChanges(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults); - - virtual void GetLastChange(std::list& target /*out*/); - - virtual SQLite::ITransaction* StartTransaction() - { - return new SQLite::Transaction(db_); - } - - virtual void FlushToDisk() - { - db_.FlushToDisk(); - } - - virtual bool HasFlushToDisk() const - { - return true; - } - - virtual void ClearChanges() - { - ClearTable("Changes"); - } - - virtual void ClearExportedResources() - { - ClearTable("ExportedResources"); - } - - virtual void GetAllMetadata(std::map& target, - int64_t id); - - virtual unsigned int GetDatabaseVersion() - { - return version_; - } - - virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea); - - - /** - * The methods declared below are for unit testing only! - **/ - - const char* GetErrorMessage() const - { - return db_.GetErrorMessage(); - } - - 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. - **/ - - virtual void SetGlobalProperty(GlobalProperty property, - const std::string& value); - - virtual bool LookupGlobalProperty(std::string& target, - GlobalProperty property); - - virtual int64_t CreateResource(const std::string& publicId, - ResourceType type); - - virtual bool LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId); - - virtual void AttachChild(int64_t parent, - int64_t child); - - virtual void SetMetadata(int64_t id, - MetadataType type, - const std::string& value); - - virtual void DeleteMetadata(int64_t id, - MetadataType type); - - virtual bool LookupMetadata(std::string& target, - int64_t id, - MetadataType type); - - virtual void ListAvailableMetadata(std::list& target, - int64_t id); - - virtual void AddAttachment(int64_t id, - const FileInfo& attachment); - - virtual void DeleteAttachment(int64_t id, - FileContentType attachment); - - virtual void ListAvailableAttachments(std::list& target, - int64_t id); - - virtual bool LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType); - - virtual void ClearMainDicomTags(int64_t id); - - virtual void SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value); - - virtual void SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value); - - virtual void GetMainDicomTags(DicomMap& map, - int64_t id); - - virtual void GetChildrenPublicId(std::list& target, - int64_t id); - - virtual void GetChildrenInternalId(std::list& target, - int64_t id); - - virtual void LogChange(int64_t internalId, - const ServerIndexChange& change); - - virtual void LogExportedResource(const ExportedResource& resource); - - virtual void GetExportedResources(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults); - - virtual void GetLastExportedResource(std::list& target /*out*/); - - virtual uint64_t GetTotalCompressedSize(); - - virtual uint64_t GetTotalUncompressedSize(); - - virtual uint64_t GetResourceCount(ResourceType resourceType); - - virtual void GetAllInternalIds(std::list& target, - ResourceType resourceType); - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType); - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit); - - virtual bool SelectPatientToRecycle(int64_t& internalId); - - virtual bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid); - - virtual bool IsProtectedPatient(int64_t internalId); - - virtual void SetProtectedPatient(int64_t internalId, - bool isProtected); - - virtual bool IsExistingResource(int64_t internalId); - - virtual void LookupIdentifier(std::list& result, - ResourceType level, - const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value); - - virtual void LookupIdentifierRange(std::list& result, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end); - }; -} diff -r ff65c925f57a -r 19ebb606910d OrthancServer/IDatabaseWrapper.h --- a/OrthancServer/IDatabaseWrapper.h Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/IDatabaseWrapper.h Wed Dec 19 13:10:48 2018 +0100 @@ -34,11 +34,14 @@ #pragma once #include "../Core/DicomFormat/DicomMap.h" -#include "../Core/SQLite/ITransaction.h" +#include "../Core/FileStorage/FileInfo.h" #include "../Core/FileStorage/IStorageArea.h" -#include "../Core/FileStorage/FileInfo.h" +#include "../Core/SQLite/ITransaction.h" + +#include "ExportedResource.h" #include "IDatabaseListener.h" -#include "ExportedResource.h" +#include "Search/DatabaseConstraint.h" +#include "Search/DatabaseLookup.h" #include #include @@ -48,6 +51,21 @@ 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; + }; + + virtual ~IDatabaseWrapper() { } @@ -198,7 +216,7 @@ virtual void SetProtectedPatient(int64_t internalId, bool isProtected) = 0; - virtual SQLite::ITransaction* StartTransaction() = 0; + virtual ITransaction* StartTransaction() = 0; virtual void SetListener(IDatabaseListener& listener) = 0; @@ -206,5 +224,13 @@ virtual void Upgrade(unsigned int targetVersion, IStorageArea& storageArea) = 0; + + virtual bool IsDiskSizeAbove(uint64_t threshold) = 0; + + virtual void ApplyLookupResources(std::vector& resourcesId, + std::vector& instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) = 0; }; } diff -r ff65c925f57a -r 19ebb606910d OrthancServer/InstallTrackAttachmentsSize.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/InstallTrackAttachmentsSize.sql Wed Dec 19 13:10:48 2018 +0100 @@ -0,0 +1,22 @@ +CREATE TABLE GlobalIntegers( + key INTEGER PRIMARY KEY, + value INTEGER); + +INSERT INTO GlobalProperties VALUES (6, 1); -- GlobalProperty_GetTotalSizeIsFast + +INSERT INTO GlobalIntegers SELECT 0, IFNULL(SUM(compressedSize), 0) FROM AttachedFiles; +INSERT INTO GlobalIntegers SELECT 1, IFNULL(SUM(uncompressedSize), 0) FROM AttachedFiles; + +CREATE TRIGGER AttachedFileIncrementSize +AFTER INSERT ON AttachedFiles +BEGIN + UPDATE GlobalIntegers SET value = value + new.compressedSize WHERE key = 0; + UPDATE GlobalIntegers SET value = value + new.uncompressedSize WHERE key = 1; +END; + +CREATE TRIGGER AttachedFileDecrementSize +AFTER DELETE ON AttachedFiles +BEGIN + UPDATE GlobalIntegers SET value = value - old.compressedSize WHERE key = 0; + UPDATE GlobalIntegers SET value = value - old.uncompressedSize WHERE key = 1; +END; diff -r ff65c925f57a -r 19ebb606910d OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -615,6 +615,7 @@ **/ LookupResource lookup(level); + DatabaseLookup lookup2; bool caseSensitivePN; @@ -655,6 +656,7 @@ } lookup.AddDicomConstraint(tag, value, sensitive); + lookup2.AddDicomConstraint(tag, value, sensitive, true /* mandatory */); } else { @@ -672,7 +674,7 @@ LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn); - context_.Apply(visitor, lookup, 0 /* "since" is not relevant to C-FIND */, limit); + context_.Apply(visitor, lookup, lookup2, 0 /* "since" is not relevant to C-FIND */, limit); } diff -r ff65c925f57a -r 19ebb606910d OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/OrthancInitialization.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -45,7 +45,7 @@ #include "../Core/Logging.h" #include "../Core/OrthancException.h" -#include "DatabaseWrapper.h" +#include "SQLiteDatabaseWrapper.h" #include "OrthancConfiguration.h" #include // For dcmDisableGethostbyaddr() @@ -308,7 +308,7 @@ { } - return new DatabaseWrapper(indexDirectory.string() + "/index"); + return new SQLiteDatabaseWrapper(indexDirectory.string() + "/index"); } diff -r ff65c925f57a -r 19ebb606910d OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -40,6 +40,7 @@ #include "../../Core/HttpServer/HttpContentNegociation.h" #include "../../Core/Logging.h" #include "../OrthancConfiguration.h" +#include "../Search/DatabaseLookup.h" #include "../Search/LookupResource.h" #include "../ServerContext.h" #include "../ServerToolbox.h" @@ -1403,6 +1404,7 @@ std::string level = request[KEY_LEVEL].asString(); LookupResource query(StringToResourceType(level.c_str())); + DatabaseLookup query2; Json::Value::Members members = request[KEY_QUERY].getMemberNames(); for (size_t i = 0; i < members.size(); i++) @@ -1416,10 +1418,13 @@ query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), request[KEY_QUERY][members[i]].asString(), caseSensitive); + query2.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), + request[KEY_QUERY][members[i]].asString(), + caseSensitive, true); } FindVisitor visitor; - context.Apply(visitor, query, since, limit); + context.Apply(visitor, query, query2, since, limit); visitor.Answer(call.GetOutput(), context.GetIndex(), query.GetLevel(), expand); } } diff -r ff65c925f57a -r 19ebb606910d OrthancServer/SQLiteDatabaseWrapper.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/SQLiteDatabaseWrapper.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -0,0 +1,1610 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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 "SQLiteDatabaseWrapper.h" + +#include "../Core/DicomFormat/DicomArray.h" +#include "../Core/Logging.h" +#include "../Core/SQLite/Transaction.h" +#include "EmbeddedResources.h" +#include "ServerToolbox.h" + +#include +#include + +namespace Orthanc +{ + namespace Internals + { + class SignalFileDeleted : public SQLite::IScalarFunction + { + private: + IDatabaseListener& listener_; + + public: + SignalFileDeleted(IDatabaseListener& listener) : + listener_(listener) + { + } + + virtual const char* GetName() const + { + return "SignalFileDeleted"; + } + + virtual unsigned int GetCardinality() const + { + return 7; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + std::string uncompressedMD5, compressedMD5; + + if (!context.IsNullValue(5)) + { + uncompressedMD5 = context.GetStringValue(5); + } + + if (!context.IsNullValue(6)) + { + compressedMD5 = context.GetStringValue(6); + } + + FileInfo info(context.GetStringValue(0), + static_cast(context.GetIntValue(1)), + static_cast(context.GetInt64Value(2)), + uncompressedMD5, + 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); + } + }; + + 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"; + } + + virtual unsigned int GetCardinality() const + { + return 2; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + VLOG(1) << "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_; + } + }; + } + + + void SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::GetExportedResourcesInternal(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); + 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 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); + + childrenPublicIds.clear(); + while (s.Step()) + { + childrenPublicIds.push_back(s.ColumnString(0)); + } + } + + + void SQLiteDatabaseWrapper::DeleteResource(int64_t id) + { + 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()) + { + target = s.ColumnString(0); + return true; + } + else + { + return false; + } + } + + + int64_t SQLiteDatabaseWrapper::GetTableRecordCount(const std::string& table) + { + char buf[128]; + sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str()); + SQLite::Statement s(db_, buf); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_InternalError); + } + + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + + return c; + } + + + SQLiteDatabaseWrapper::SQLiteDatabaseWrapper(const std::string& path) : + listener_(NULL), + signalRemainingAncestor_(NULL), + version_(0) + { + db_.Open(path); + } + + + SQLiteDatabaseWrapper::SQLiteDatabaseWrapper() : + listener_(NULL), + signalRemainingAncestor_(NULL), + version_(0) + { + db_.OpenInMemory(); + } + + + void SQLiteDatabaseWrapper::Open() + { + 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"); + + // Make "LIKE" case-sensitive in SQLite + db_.Execute("PRAGMA case_sensitive_like = true;"); + + if (!db_.DoesTableExist("GlobalProperties")) + { + LOG(INFO) << "Creating the database"; + std::string query; + EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE); + db_.Execute(query); + } + + // Check the version of the database + std::string tmp; + if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion)) + { + tmp = "Unknown"; + } + + bool ok = false; + try + { + LOG(INFO) << "Version of the Orthanc database: " << tmp; + version_ = boost::lexical_cast(tmp); + ok = true; + } + catch (boost::bad_lexical_cast&) + { + } + + if (!ok) + { + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, + "Incompatible version of the Orthanc database: " + tmp); + } + + // New in Orthanc 1.5.1 + if (version_ == 6) + { + if (!LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) || + tmp != "1") + { + LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments"; + std::string query; + EmbeddedResources::GetFileResource(query, EmbeddedResources::INSTALL_TRACK_ATTACHMENTS_SIZE); + db_.Execute(query); + } + } + + signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; + db_.Register(signalRemainingAncestor_); + } + + + static void ExecuteUpgradeScript(SQLite::Connection& db, + EmbeddedResources::FileResourceId script) + { + std::string upgrade; + EmbeddedResources::GetFileResource(upgrade, script); + db.BeginTransaction(); + db.Execute(upgrade); + db.CommitTransaction(); + } + + + void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion, + IStorageArea& storageArea) + { + if (targetVersion != 6) + { + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); + } + + // This version of Orthanc is only compatible with versions 3, 4, + // 5 and 6 of the DB schema + if (version_ != 3 && + version_ != 4 && + version_ != 5 && + version_ != 6) + { + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); + } + + if (version_ == 3) + { + LOG(WARNING) << "Upgrading database version from 3 to 4"; + ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_3_TO_4); + version_ = 4; + } + + if (version_ == 4) + { + LOG(WARNING) << "Upgrading database version from 4 to 5"; + ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5); + version_ = 5; + } + + if (version_ == 5) + { + LOG(WARNING) << "Upgrading database version from 5 to 6"; + // 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(); + 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) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT parentId FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + if (s.ColumnIsNull(0)) + { + return false; + } + else + { + parentId = s.ColumnInt(0); + return true; + } + } + + + 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) + { + 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); + } + + + class SQLiteDatabaseWrapper::Transaction : public IDatabaseWrapper::ITransaction + { + private: + SQLiteDatabaseWrapper& that_; + std::auto_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) + { + 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(); + } + + + 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) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); + s.BindInt64(0, parent); + s.BindInt64(1, child); + s.Run(); + } + + + 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::ListAvailableMetadata(std::list& target, + int64_t id) + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + target.push_back(static_cast(s.ColumnInt(0))); + } + } + + + 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::list& 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.push_back(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) + { + 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(); + } + + + void SQLiteDatabaseWrapper::GetMainDicomTags(DicomMap& map, + int64_t id) + { + 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 = ?"); + 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.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() + { + // 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 + { + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + return c; + } + } + + + void SQLiteDatabaseWrapper::GetAllInternalIds(std::list& target, + ResourceType resourceType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + target.clear(); + while (s.Step()) + { + target.push_back(s.ColumnInt64(0)); + } + } + + + 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; + } + + 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(); + } + + + void SQLiteDatabaseWrapper::LookupIdentifier(std::list& target, + ResourceType level, + const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) + { + static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " + "d.id = r.internalId AND r.resourceType=? AND " + "d.tagGroup=? AND d.tagElement=? AND "); + + std::auto_ptr s; + + switch (type) + { + case IdentifierConstraintType_GreaterOrEqual: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?")); + break; + + case IdentifierConstraintType_SmallerOrEqual: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?")); + break; + + case IdentifierConstraintType_Wildcard: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?")); + break; + + case IdentifierConstraintType_Equal: + default: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?")); + break; + } + + assert(s.get() != NULL); + + s->BindInt(0, level); + s->BindInt(1, tag.GetGroup()); + s->BindInt(2, tag.GetElement()); + s->BindString(3, value); + + target.clear(); + + while (s->Step()) + { + target.push_back(s->ColumnInt64(0)); + } + } + + + void SQLiteDatabaseWrapper::LookupIdentifierRange(std::list& target, + ResourceType level, + const DicomTag& tag, + const std::string& start, + const std::string& end) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " + "d.id = r.internalId AND r.resourceType=? AND " + "d.tagGroup=? AND d.tagElement=? AND d.value>=? AND d.value<=?"); + + statement.BindInt(0, level); + statement.BindInt(1, tag.GetGroup()); + statement.BindInt(2, tag.GetElement()); + statement.BindString(3, start); + statement.BindString(4, end); + + target.clear(); + + while (statement.Step()) + { + target.push_back(statement.ColumnInt64(0)); + } + } + + + bool SQLiteDatabaseWrapper::IsDiskSizeAbove(uint64_t threshold) + { + return GetTotalCompressedSize() > threshold; + } + + + static std::string FormatLevel(ResourceType level) + { + switch (level) + { + case ResourceType_Patient: + return "patients"; + + case ResourceType_Study: + return "studies"; + + case ResourceType_Series: + return "series"; + + case ResourceType_Instance: + return "instances"; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + static bool FormatComparison(std::string& target, + const DatabaseConstraint& constraint, + size_t index, + std::vector& parameters) + { + std::string tag = "t" + boost::lexical_cast(index); + + std::string comparison; + + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + case ConstraintType_SmallerOrEqual: + case ConstraintType_GreaterOrEqual: + { + std::string op; + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + op = "="; + break; + + case ConstraintType_SmallerOrEqual: + op = "<="; + break; + + case ConstraintType_GreaterOrEqual: + op = ">="; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + parameters.push_back(constraint.GetSingleValue()); + + if (constraint.IsCaseSensitive()) + { + comparison = tag + ".value " + op + " ?"; + } + else + { + comparison = "lower(" + tag + ".value) " + op + " lower(?)"; + } + + break; + } + + case ConstraintType_List: + { + for (size_t i = 0; i < constraint.GetValuesCount(); i++) + { + parameters.push_back(constraint.GetValue(i)); + + if (!comparison.empty()) + { + comparison += ", "; + } + + if (constraint.IsCaseSensitive()) + { + comparison += "?"; + } + else + { + comparison += "lower(?)"; + } + } + + if (constraint.IsCaseSensitive()) + { + comparison = tag + ".value IN (" + comparison + ")"; + } + else + { + comparison = "lower(" + tag + ".value) IN (" + comparison + ")"; + } + + break; + } + + case ConstraintType_Wildcard: + { + const std::string value = constraint.GetSingleValue(); + + if (value == "*") + { + if (!constraint.IsMandatory()) + { + // Universal constraint on an optional tag, ignore it + return false; + } + } + else + { + std::string escaped; + escaped.reserve(value.size()); + + for (size_t i = 0; i < value.size(); i++) + { + if (value[i] == '*') + { + escaped += "%"; + } + else if (value[i] == '?') + { + escaped += "_"; + } + else if (value[i] == '%') + { + escaped += "\\%"; + } + else if (value[i] == '_') + { + escaped += "\\_"; + } + else if (value[i] == '\\') + { + escaped += "\\\\"; + } + else + { + escaped += value[i]; + } + } + + parameters.push_back(escaped); + + if (constraint.IsCaseSensitive()) + { + comparison = tag + ".value LIKE ? ESCAPE '\\'"; + } + else + { + comparison = "lower(" + tag + ".value) LIKE lower(?) ESCAPE '\\'"; + } + } + + break; + } + + default: + // Don't modify "parameters" in this case! + return false; + } + + if (constraint.IsMandatory()) + { + target = comparison; + } + else if (comparison.empty()) + { + target = tag + ".value IS NULL"; + } + else + { + target = tag + ".value IS NULL OR " + comparison; + } + + return true; + } + + + static void FormatJoin(std::string& target, + const DatabaseConstraint& constraint, + size_t index) + { + std::string tag = "t" + boost::lexical_cast(index); + + if (constraint.IsMandatory()) + { + target = " INNER JOIN "; + } + else + { + target = " LEFT JOIN "; + } + + if (constraint.IsIdentifier()) + { + target += "DicomIdentifiers "; + } + else + { + target += "MainDicomTags "; + } + + target += (tag + " ON " + tag + ".id = " + FormatLevel(constraint.GetLevel()) + + ".internalId AND " + tag + ".tagGroup = " + + boost::lexical_cast(constraint.GetTag().GetGroup()) + + " AND " + tag + ".tagElement = " + + boost::lexical_cast(constraint.GetTag().GetElement())); + } + + + static void AnswerLookup(std::vector& resourcesId, + std::vector& instancesId, + SQLite::Connection& db, + ResourceType level) + { + resourcesId.clear(); + instancesId.clear(); + + std::auto_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::vector& resourcesId, + std::vector& instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + { + for (size_t i = 0; i < lookup.size(); i++) + { + std::cout << i << ": " << lookup[i].GetTag() << " - " << EnumerationToString(lookup[i].GetLevel()); + std::cout << std::endl; + } + + assert(ResourceType_Patient < ResourceType_Study && + ResourceType_Study < ResourceType_Series && + ResourceType_Series < ResourceType_Instance); + + ResourceType upperLevel = queryLevel; + ResourceType lowerLevel = queryLevel; + + for (size_t i = 0; i < lookup.size(); i++) + { + ResourceType level = lookup[i].GetLevel(); + + if (level < upperLevel) + { + upperLevel = level; + } + + if (level > lowerLevel) + { + lowerLevel = level; + } + } + + printf("ICI 2: [%s] -> [%s]\n", EnumerationToString(upperLevel), EnumerationToString(lowerLevel)); + + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup"); + s.Run(); + } + + std::string joins, comparisons; + std::vector parameters; + + size_t count = 0; + + for (size_t i = 0; i < lookup.size(); i++) + { + std::string comparison; + + if (FormatComparison(comparison, lookup[i], count, parameters)) + { + std::string join; + FormatJoin(join, lookup[i], count); + joins += join; + + if (!comparison.empty()) + { + comparisons += " AND " + comparison; + } + + count ++; + } + } + + { + std::string sql = ("CREATE TEMPORARY TABLE Lookup AS SELECT " + + FormatLevel(queryLevel) + ".publicId, " + + FormatLevel(queryLevel) + ".internalId" + + " FROM Resources AS " + FormatLevel(queryLevel)); + + for (int level = queryLevel - 1; level >= upperLevel; level--) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast(level)) + " ON " + + FormatLevel(static_cast(level)) + ".internalId=" + + FormatLevel(static_cast(level + 1)) + ".parentId"); + } + + for (int level = queryLevel + 1; level <= lowerLevel; level++) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast(level)) + " ON " + + FormatLevel(static_cast(level - 1)) + ".internalId=" + + FormatLevel(static_cast(level)) + ".parentId"); + } + + sql += (joins + " WHERE " + FormatLevel(queryLevel) + ".resourceType = " + + boost::lexical_cast(queryLevel) + comparisons); + + if (limit != 0) + { + sql += " LIMIT " + boost::lexical_cast(limit); + } + + printf("[%s]\n", sql.c_str()); + + SQLite::Statement s(db_, sql); + + for (size_t i = 0; i < parameters.size(); i++) + { + printf(" %lu = '%s'\n", i, parameters[i].c_str()); + s.BindString(i, parameters[i]); + } + + s.Run(); + } + + AnswerLookup(resourcesId, instancesId, db_, queryLevel); + } +} diff -r ff65c925f57a -r 19ebb606910d OrthancServer/SQLiteDatabaseWrapper.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/SQLiteDatabaseWrapper.h Wed Dec 19 13:10:48 2018 +0100 @@ -0,0 +1,286 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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 "IDatabaseWrapper.h" + +#include "../Core/SQLite/Connection.h" + +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 + { + private: + class Transaction; + + IDatabaseListener* listener_; + SQLite::Connection db_; + Internals::SignalRemainingAncestor* signalRemainingAncestor_; + unsigned int version_; + + void GetChangesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults); + + void GetExportedResourcesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults); + + void ClearTable(const std::string& tableName); + + public: + SQLiteDatabaseWrapper(const std::string& path); + + SQLiteDatabaseWrapper(); + + virtual void Open(); + + virtual void Close() + { + db_.Close(); + } + + virtual void SetListener(IDatabaseListener& listener); + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId); + + virtual std::string GetPublicId(int64_t resourceId); + + virtual ResourceType GetResourceType(int64_t resourceId); + + virtual void DeleteResource(int64_t id); + + virtual void GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults); + + virtual void GetLastChange(std::list& target /*out*/); + + virtual IDatabaseWrapper::ITransaction* StartTransaction(); + + virtual void FlushToDisk() + { + db_.FlushToDisk(); + } + + virtual bool HasFlushToDisk() const + { + return true; + } + + virtual void ClearChanges() + { + ClearTable("Changes"); + } + + virtual void ClearExportedResources() + { + ClearTable("ExportedResources"); + } + + virtual void GetAllMetadata(std::map& target, + int64_t id); + + virtual unsigned int GetDatabaseVersion() + { + return version_; + } + + virtual void Upgrade(unsigned int targetVersion, + IStorageArea& storageArea); + + + /** + * The methods declared below are for unit testing only! + **/ + + const char* GetErrorMessage() const + { + return db_.GetErrorMessage(); + } + + 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. + **/ + + virtual void SetGlobalProperty(GlobalProperty property, + const std::string& value); + + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property); + + virtual int64_t CreateResource(const std::string& publicId, + ResourceType type); + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId); + + virtual void AttachChild(int64_t parent, + int64_t child); + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value); + + virtual void DeleteMetadata(int64_t id, + MetadataType type); + + virtual bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type); + + virtual void ListAvailableMetadata(std::list& target, + int64_t id); + + virtual void AddAttachment(int64_t id, + const FileInfo& attachment); + + virtual void DeleteAttachment(int64_t id, + FileContentType attachment); + + virtual void ListAvailableAttachments(std::list& target, + int64_t id); + + virtual bool LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType); + + virtual void ClearMainDicomTags(int64_t id); + + virtual void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value); + + virtual void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value); + + virtual void GetMainDicomTags(DicomMap& map, + int64_t id); + + virtual void GetChildrenPublicId(std::list& target, + int64_t id); + + virtual void GetChildrenInternalId(std::list& target, + int64_t id); + + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change); + + virtual void LogExportedResource(const ExportedResource& resource); + + virtual void GetExportedResources(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults); + + virtual void GetLastExportedResource(std::list& target /*out*/); + + virtual uint64_t GetTotalCompressedSize(); + + virtual uint64_t GetTotalUncompressedSize(); + + virtual uint64_t GetResourceCount(ResourceType resourceType); + + virtual void GetAllInternalIds(std::list& target, + ResourceType resourceType); + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType); + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit); + + virtual bool SelectPatientToRecycle(int64_t& internalId); + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid); + + virtual bool IsProtectedPatient(int64_t internalId); + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected); + + virtual bool IsExistingResource(int64_t internalId); + + virtual void LookupIdentifier(std::list& result, + ResourceType level, + const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value); + + virtual void LookupIdentifierRange(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& start, + const std::string& end); + + virtual bool IsDiskSizeAbove(uint64_t threshold); + + virtual void ApplyLookupResources(std::vector& resourcesId, + std::vector& instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit); + }; +} diff -r ff65c925f57a -r 19ebb606910d OrthancServer/Search/DatabaseConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DatabaseConstraint.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -0,0 +1,108 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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 "DatabaseConstraint.h" + +#include "../../Core/OrthancException.h" +#include "../ServerToolbox.h" + +namespace Orthanc +{ + DatabaseConstraint::DatabaseConstraint(const DicomTagConstraint& constraint, + ResourceType level, + DicomTagType tagType) : + level_(level), + tag_(constraint.GetTag()), + constraintType_(constraint.GetConstraintType()), + mandatory_(constraint.IsMandatory()) + { + switch (tagType) + { + case DicomTagType_Identifier: + isIdentifier_ = true; + caseSensitive_ = true; + break; + + case DicomTagType_Main: + isIdentifier_ = false; + caseSensitive_ = constraint.IsCaseSensitive(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + values_.reserve(constraint.GetValues().size()); + + for (std::set::const_iterator + it = constraint.GetValues().begin(); + it != constraint.GetValues().end(); ++it) + { + if (isIdentifier_) + { + values_.push_back(ServerToolbox::NormalizeIdentifier(*it)); + } + else + { + values_.push_back(*it); + } + } + } + + + const std::string& DatabaseConstraint::GetValue(size_t index) const + { + if (index >= values_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return values_[index]; + } + } + + + const std::string& DatabaseConstraint::GetSingleValue() const + { + if (values_.size() != 1) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return values_[0]; + } + } +} diff -r ff65c925f57a -r 19ebb606910d OrthancServer/Search/DatabaseConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DatabaseConstraint.h Wed Dec 19 13:10:48 2018 +0100 @@ -0,0 +1,95 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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 "DicomTagConstraint.h" + +namespace Orthanc +{ + class DatabaseConstraint + { + private: + ResourceType level_; + DicomTag tag_; + bool isIdentifier_; + ConstraintType constraintType_; + std::vector values_; + bool caseSensitive_; + bool mandatory_; + + public: + DatabaseConstraint(const DicomTagConstraint& constraint, + ResourceType level, + DicomTagType tagType); + + ResourceType GetLevel() const + { + return level_; + } + + const DicomTag& GetTag() const + { + return tag_; + } + + bool IsIdentifier() const + { + return isIdentifier_; + } + + ConstraintType GetConstraintType() const + { + return constraintType_; + } + + size_t GetValuesCount() const + { + return values_.size(); + } + + const std::string& GetValue(size_t index) const; + + const std::string& GetSingleValue() const; + + bool IsCaseSensitive() const + { + return caseSensitive_; + } + + bool IsMandatory() const + { + return mandatory_; + } + }; +} diff -r ff65c925f57a -r 19ebb606910d OrthancServer/Search/DatabaseLookup.cpp --- a/OrthancServer/Search/DatabaseLookup.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/Search/DatabaseLookup.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -39,50 +39,6 @@ namespace Orthanc { - void DatabaseLookup::LoadTags(ResourceType level) - { - const DicomTag* tags = NULL; - size_t size; - - ServerToolbox::LoadIdentifiers(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - if (tags_.find(tags[i]) == tags_.end()) - { - tags_[tags[i]] = TagInfo(DicomTagType_Identifier, level); - } - 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)); - } - } - - DicomMap::LoadMainDicomTags(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - if (tags_.find(tags[i]) == tags_.end()) - { - tags_[tags[i]] = TagInfo(DicomTagType_Main, level); - } - } - } - - - DatabaseLookup::DatabaseLookup() - { - LoadTags(ResourceType_Patient); - LoadTags(ResourceType_Study); - LoadTags(ResourceType_Series); - LoadTags(ResourceType_Instance); - } - - DatabaseLookup::~DatabaseLookup() { for (size_t i = 0; i < constraints_.size(); i++) @@ -116,17 +72,6 @@ else { constraints_.push_back(constraint); - - std::map::const_iterator tag = tags_.find(constraint->GetTag()); - - if (tag == tags_.end()) - { - constraint->SetTagInfo(DicomTagType_Generic, ResourceType_Instance); - } - else - { - constraint->SetTagInfo(tag->second.GetType(), tag->second.GetLevel()); - } } } @@ -146,9 +91,83 @@ } + void DatabaseLookup::AddDicomConstraintInternal(const DicomTag& tag, + ValueRepresentation vr, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag) + { + if ((vr == ValueRepresentation_Date || + vr == ValueRepresentation_DateTime || + vr == ValueRepresentation_Time) && + dicomQuery.find('-') != std::string::npos) + { + /** + * Range matching is only defined for TM, DA and DT value + * representations. This code fixes issues 35 and 37. + * + * Reference: "Range matching is not defined for types of + * Attributes other than dates and times", DICOM PS 3.4, + * C.2.2.2.5 ("Range Matching"). + **/ + size_t separator = dicomQuery.find('-'); + std::string lower = dicomQuery.substr(0, separator); + std::string upper = dicomQuery.substr(separator + 1); + + if (!lower.empty()) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive, mandatoryTag)); + } + + if (!upper.empty()) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag)); + } + } + else if (dicomQuery.find('\\') != std::string::npos) + { + DicomTag fixedTag(tag); + + if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + { + // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained + // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html + fixedTag = DICOM_TAG_MODALITY; + } + + std::auto_ptr constraint + (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive, mandatoryTag)); + + std::vector items; + Toolbox::TokenizeString(items, dicomQuery, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + constraint->AddValue(items[i]); + } + + AddConstraint(constraint.release()); + } + else if (dicomQuery.find('*') != std::string::npos || + dicomQuery.find('?') != std::string::npos) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive, mandatoryTag)); + } + else + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag)); + } + } + + void DatabaseLookup::AddDicomConstraint(const DicomTag& tag, const std::string& dicomQuery, - bool caseSensitivePN) + bool caseSensitivePN, + bool mandatoryTag) { ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); @@ -188,75 +207,24 @@ * (0020,000D) UI StudyInstanceUID => Case-sensitive * (0020,000E) UI SeriesInstanceUID => Case-sensitive **/ - bool caseSensitive = true; + if (vr == ValueRepresentation_PersonName) { - caseSensitive = caseSensitivePN; - } - - if ((vr == ValueRepresentation_Date || - vr == ValueRepresentation_DateTime || - vr == ValueRepresentation_Time) && - dicomQuery.find('-') != std::string::npos) - { - /** - * Range matching is only defined for TM, DA and DT value - * representations. This code fixes issues 35 and 37. - * - * Reference: "Range matching is not defined for types of - * Attributes other than dates and times", DICOM PS 3.4, - * C.2.2.2.5 ("Range Matching"). - **/ - size_t separator = dicomQuery.find('-'); - std::string lower = dicomQuery.substr(0, separator); - std::string upper = dicomQuery.substr(separator + 1); - - if (!lower.empty()) - { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive)); - } - - if (!upper.empty()) - { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive)); - } - } - else if (dicomQuery.find('\\') != std::string::npos) - { - DicomTag fixedTag(tag); - - if (tag == DICOM_TAG_MODALITIES_IN_STUDY) - { - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained - // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html - fixedTag = DICOM_TAG_MODALITY; - } - - std::auto_ptr constraint - (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive)); - - std::vector items; - Toolbox::TokenizeString(items, dicomQuery, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - constraint->AddValue(items[i]); - } - - AddConstraint(constraint.release()); - } - else if (dicomQuery.find('*') != std::string::npos || - dicomQuery.find('?') != std::string::npos) - { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive)); + AddDicomConstraintInternal(tag, vr, dicomQuery, caseSensitivePN, mandatoryTag); } else { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_Equal, dicomQuery, caseSensitive)); + AddDicomConstraintInternal(tag, vr, dicomQuery, true /* case sensitive */, mandatoryTag); } } + + + void DatabaseLookup::AddRestConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag) + { + AddDicomConstraintInternal(tag, FromDcmtkBridge::LookupValueRepresentation(tag), + dicomQuery, caseSensitive, mandatoryTag); + } } diff -r ff65c925f57a -r 19ebb606910d OrthancServer/Search/DatabaseLookup.h --- a/OrthancServer/Search/DatabaseLookup.h Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/Search/DatabaseLookup.h Wed Dec 19 13:10:48 2018 +0100 @@ -40,44 +40,17 @@ class DatabaseLookup : public boost::noncopyable { private: - class TagInfo - { - private: - DicomTagType type_; - ResourceType level_; - - public: - TagInfo() : - type_(DicomTagType_Generic), - level_(ResourceType_Instance) - { - } + std::vector constraints_; - TagInfo(DicomTagType type, - ResourceType level) : - type_(type), - level_(level) - { - } - - DicomTagType GetType() const - { - return type_; - } - - ResourceType GetLevel() const - { - return level_; - } - }; - - std::vector constraints_; - std::map tags_; - - void LoadTags(ResourceType level); - + void AddDicomConstraintInternal(const DicomTag& tag, + ValueRepresentation vr, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag); public: - DatabaseLookup(); + DatabaseLookup() + { + } ~DatabaseLookup(); @@ -99,6 +72,12 @@ void AddDicomConstraint(const DicomTag& tag, const std::string& dicomQuery, - bool caseSensitivePN); + bool caseSensitivePN, + bool mandatoryTag); + + void AddRestConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag); }; } diff -r ff65c925f57a -r 19ebb606910d OrthancServer/Search/DicomTagConstraint.cpp --- a/OrthancServer/Search/DicomTagConstraint.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/Search/DicomTagConstraint.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -97,13 +97,12 @@ DicomTagConstraint::DicomTagConstraint(const DicomTag& tag, ConstraintType type, const std::string& value, - bool caseSensitive) : - hasTagInfo_(false), - tagType_(DicomTagType_Generic), // Dummy initialization - level_(ResourceType_Patient), // Dummy initialization + bool caseSensitive, + bool mandatory) : tag_(tag), constraintType_(type), - caseSensitive_(caseSensitive) + caseSensitive_(caseSensitive), + mandatory_(mandatory) { if (type == ConstraintType_Equal || type == ConstraintType_SmallerOrEqual || @@ -128,13 +127,12 @@ DicomTagConstraint::DicomTagConstraint(const DicomTag& tag, ConstraintType type, - bool caseSensitive) : - hasTagInfo_(false), - tagType_(DicomTagType_Generic), // Dummy initialization - level_(ResourceType_Patient), // Dummy initialization + bool caseSensitive, + bool mandatory) : tag_(tag), constraintType_(type), - caseSensitive_(caseSensitive) + caseSensitive_(caseSensitive), + mandatory_(mandatory) { if (type != ConstraintType_List) { @@ -143,41 +141,6 @@ } - void DicomTagConstraint::SetTagInfo(DicomTagType tagType, - ResourceType level) - { - hasTagInfo_ = true; - tagType_ = tagType; - level_ = level; - } - - - DicomTagType DicomTagConstraint::GetTagType() const - { - if (!hasTagInfo_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - return tagType_; - } - } - - - const ResourceType DicomTagConstraint::GetLevel() const - { - if (!hasTagInfo_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - return level_; - } - } - - void DicomTagConstraint::AddValue(const std::string& value) { if (constraintType_ != ConstraintType_List) @@ -268,8 +231,18 @@ const DicomValue* tmp = value.TestAndGetValue(tag_); if (tmp == NULL || - tmp->IsNull() || - tmp->IsBinary()) + tmp->IsNull()) + { + if (mandatory_) + { + return false; + } + else + { + return true; + } + } + else if (tmp->IsBinary()) { return false; } diff -r ff65c925f57a -r 19ebb606910d OrthancServer/Search/DicomTagConstraint.h --- a/OrthancServer/Search/DicomTagConstraint.h Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/Search/DicomTagConstraint.h Wed Dec 19 13:10:48 2018 +0100 @@ -46,13 +46,11 @@ class NormalizedString; class RegularExpression; - bool hasTagInfo_; - DicomTagType tagType_; - ResourceType level_; DicomTag tag_; ConstraintType constraintType_; std::set values_; bool caseSensitive_; + bool mandatory_; boost::shared_ptr regex_; @@ -60,23 +58,14 @@ DicomTagConstraint(const DicomTag& tag, ConstraintType type, const std::string& value, - bool caseSensitive); + bool caseSensitive, + bool mandatory); + // For list search DicomTagConstraint(const DicomTag& tag, ConstraintType type, - bool caseSensitive); - - bool HasTagInfo() const - { - return hasTagInfo_; - } - - void SetTagInfo(DicomTagType tagType, - ResourceType level); - - DicomTagType GetTagType() const; - - const ResourceType GetLevel() const; + bool caseSensitive, + bool mandatory); const DicomTag& GetTag() const { @@ -93,6 +82,16 @@ return caseSensitive_; } + void SetCaseSensitive(bool caseSensitive) + { + caseSensitive_ = caseSensitive; + } + + bool IsMandatory() const + { + return mandatory_; + } + void AddValue(const std::string& value); const std::string& GetValue() const; diff -r ff65c925f57a -r 19ebb606910d OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/ServerContext.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -775,10 +775,12 @@ void ServerContext::Apply(ILookupVisitor& visitor, const ::Orthanc::LookupResource& lookup, + const DatabaseLookup& lookup2, size_t since, size_t limit) { LookupMode mode; + unsigned int databaseLimit; { // New configuration option in 1.5.1 @@ -804,19 +806,59 @@ "Configuration option \"StorageAccessOnFind\" " "should be \"Always\", \"Never\" or \"Answers\": " + value); } + + if (lookup.GetLevel() == ResourceType_Instance) + { + databaseLimit = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0); + } + else + { + databaseLimit = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0); + } } std::vector resources, instances; GetIndex().FindCandidates(resources, instances, lookup); + bool complete = true; + +#if 1 + { + std::vector resources2, instances2; + + size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); + GetIndex().ApplyLookupResources(resources2, instances2, lookup2, lookup.GetLevel(), lookupLimit); + + if (databaseLimit != 0 && + resources2.size() > databaseLimit) + { + complete = false; + } + + // Sanity checks + std::set r; + for (size_t i = 0; i < resources2.size(); i++) + { + r.insert(resources2[i]); + } + + printf("%d %d\n", resources2.size(), resources.size()); + assert(resources2.size() >= resources.size()); + + for (size_t i = 0; i < resources.size(); i++) + { + assert(r.find(resources[i]) != r.end()); + } + } +#endif + LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size(); assert(resources.size() == instances.size()); size_t countResults = 0; size_t skipped = 0; - bool complete = true; const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded(); diff -r ff65c925f57a -r 19ebb606910d OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/ServerContext.h Wed Dec 19 13:10:48 2018 +0100 @@ -364,6 +364,7 @@ void Apply(ILookupVisitor& visitor, const ::Orthanc::LookupResource& lookup, + const DatabaseLookup& lookup2, size_t since, size_t limit); diff -r ff65c925f57a -r 19ebb606910d OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/ServerEnumerations.h Wed Dec 19 13:10:48 2018 +0100 @@ -93,8 +93,7 @@ GlobalProperty_FlushSleep = 2, GlobalProperty_AnonymizationSequence = 3, GlobalProperty_JobsRegistry = 5, - GlobalProperty_TotalCompressedSize = 6, // Reserved for Orthanc > 1.5.0 - GlobalProperty_TotalUncompressedSize = 7, // Reserved for Orthanc > 1.5.0 + GlobalProperty_GetTotalSizeIsFast = 6, // New in Orthanc 1.5.2 GlobalProperty_Modalities = 20, // New in Orthanc 1.5.0 GlobalProperty_Peers = 21, // New in Orthanc 1.5.0 diff -r ff65c925f57a -r 19ebb606910d OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/ServerIndex.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -215,7 +215,7 @@ { private: ServerIndex& index_; - std::auto_ptr transaction_; + std::auto_ptr transaction_; bool isCommitted_; public: @@ -226,8 +226,6 @@ transaction_.reset(index_.db_.StartTransaction()); transaction_->Begin(); - assert(index_.currentStorageSize_ == index_.db_.GetTotalCompressedSize()); - index_.listener_->StartTransaction(); } @@ -245,18 +243,16 @@ { if (!isCommitted_) { - transaction_->Commit(); + 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(); - index_.currentStorageSize_ += sizeOfAddedFiles; - - assert(index_.currentStorageSize_ >= index_.listener_->GetSizeOfFilesToRemove()); - index_.currentStorageSize_ -= index_.listener_->GetSizeOfFilesToRemove(); - // Send all the pending changes to the Orthanc plugins index_.listener_->CommitChanges(); @@ -303,6 +299,107 @@ }; + class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable + { + private: + class TagInfo + { + private: + ResourceType level_; + DicomTagType type_; + + public: + TagInfo() + { + } + + TagInfo(ResourceType level, + DicomTagType type) : + level_(level), + type_(type) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + DicomTagType GetType() const + { + return type_; + } + }; + + typedef std::map 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)); + } + } + + DicomMap::LoadMainDicomTags(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (registry_.find(tags[i]) == registry_.end()) + { + registry_[tags[i]] = TagInfo(level, DicomTagType_Main); + } + } + } + + public: + MainDicomTagsRegistry() + { + LoadTags(ResourceType_Patient); + LoadTags(ResourceType_Study); + LoadTags(ResourceType_Series); + LoadTags(ResourceType_Instance); + } + + void LookupTag(ResourceType& level, + DicomTagType& type, + const DicomTag& tag) const + { + Registry::const_iterator it = registry_.find(tag); + + if (it == registry_.end()) + { + // Default values + level = ResourceType_Instance; + type = DicomTagType_Generic; + } + else + { + level = it->second.GetLevel(); + type = it->second.GetType(); + } + } + }; + + bool ServerIndex::DeleteResource(Json::Value& target, const std::string& uuid, ResourceType expectedType) @@ -545,13 +642,12 @@ db_(db), maximumStorageSize_(0), maximumPatients_(0), - overwrite_(false) + overwrite_(false), + mainDicomTagsRegistry_(new MainDicomTagsRegistry) { listener_.reset(new Listener(context)); db_.SetListener(*listener_); - currentStorageSize_ = db_.GetTotalCompressedSize(); - // Initial recycling if the parameters have changed since the last // execution of Orthanc StandaloneRecycling(); @@ -877,8 +973,7 @@ boost::mutex::scoped_lock lock(mutex_); target = Json::objectValue; - uint64_t cs = currentStorageSize_; - assert(cs == db_.GetTotalCompressedSize()); + uint64_t cs = db_.GetTotalCompressedSize(); uint64_t us = db_.GetTotalUncompressedSize(); target["TotalDiskSize"] = boost::lexical_cast(cs); target["TotalUncompressedSize"] = boost::lexical_cast(us); @@ -1369,10 +1464,9 @@ { if (maximumStorageSize_ != 0) { - uint64_t currentSize = currentStorageSize_ - listener_->GetSizeOfFilesToRemove(); - assert(db_.GetTotalCompressedSize() == currentSize); - - if (currentSize + instanceSize > maximumStorageSize_) + assert(maximumStorageSize_ >= instanceSize); + + if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize)) { return true; } @@ -2460,4 +2554,54 @@ 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; + } + + DatabaseConstraint c(source.GetConstraint(i), level, type); + target.push_back(c); + } + } + } + + + void ServerIndex::ApplyLookupResources(std::vector& resourcesId, + std::vector& instancesId, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t limit) + { + std::vector normalized; + NormalizeLookup(normalized, lookup, queryLevel); + + { + boost::mutex::scoped_lock lock(mutex_); + db_.ApplyLookupResources(resourcesId, instancesId, normalized, queryLevel, limit); + } + } } diff -r ff65c925f57a -r 19ebb606910d OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/ServerIndex.h Wed Dec 19 13:10:48 2018 +0100 @@ -59,6 +59,7 @@ class Listener; class Transaction; class UnstableResourcePayload; + class MainDicomTagsRegistry; bool done_; boost::mutex mutex_; @@ -69,10 +70,10 @@ IDatabaseWrapper& db_; LeastRecentlyUsedIndex unstableResources_; - uint64_t currentStorageSize_; uint64_t maximumStorageSize_; unsigned int maximumPatients_; bool overwrite_; + std::auto_ptr mainDicomTagsRegistry_; static void FlushThread(ServerIndex* that, unsigned int threadSleep); @@ -126,6 +127,10 @@ MetadataType metadata, const std::string& value); + void NormalizeLookup(std::vector& target, + const DatabaseLookup& source, + ResourceType level) const; + public: ServerIndex(ServerContext& context, IDatabaseWrapper& database, @@ -293,5 +298,11 @@ ResourceType parentType); void ReconstructInstance(ParsedDicomFile& dicom); + + void ApplyLookupResources(std::vector& resourcesId, + std::vector& instancesId, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t limit); }; } diff -r ff65c925f57a -r 19ebb606910d OrthancServer/main.cpp --- a/OrthancServer/main.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/OrthancServer/main.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -1174,7 +1174,7 @@ else if (currentVersion != ORTHANC_DATABASE_VERSION) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, - "The database schema must be changed from version " + + "The database schema must be upgraded from version " + boost::lexical_cast(currentVersion) + " to " + boost::lexical_cast(ORTHANC_DATABASE_VERSION) + ": Please run Orthanc with the \"--upgrade\" argument"); diff -r ff65c925f57a -r 19ebb606910d Plugins/Engine/OrthancPluginDatabase.cpp --- a/Plugins/Engine/OrthancPluginDatabase.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/Plugins/Engine/OrthancPluginDatabase.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -47,6 +47,62 @@ namespace Orthanc { + class OrthancPluginDatabase::Transaction : public IDatabaseWrapper::ITransaction + { + private: + OrthancPluginDatabase& that_; + + void CheckSuccess(OrthancPluginErrorCode code) const + { + if (code != OrthancPluginErrorCode_Success) + { + that_.errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + public: + Transaction(OrthancPluginDatabase& that) : + that_(that) + { + } + + virtual void Begin() + { + CheckSuccess(that_.backend_.startTransaction(that_.payload_)); + } + + virtual void Rollback() + { + CheckSuccess(that_.backend_.rollbackTransaction(that_.payload_)); + } + + virtual void Commit(int64_t diskSizeDelta) + { + if (that_.fastGetTotalSize_) + { + CheckSuccess(that_.backend_.commitTransaction(that_.payload_)); + } + else + { + if (static_cast(that_.currentDiskSize_) + diskSizeDelta < 0) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + uint64_t newDiskSize = (that_.currentDiskSize_ + diskSizeDelta); + + assert(newDiskSize == that_.GetTotalCompressedSize()); + + CheckSuccess(that_.backend_.commitTransaction(that_.payload_)); + + // The transaction has succeeded, we can commit the new disk size + that_.currentDiskSize_ = newDiskSize; + } + } + }; + + static FileInfo Convert(const OrthancPluginAttachment& attachment) { return FileInfo(attachment.uuid, @@ -189,6 +245,35 @@ } + void OrthancPluginDatabase::Open() + { + CheckSuccess(backend_.open(payload_)); + + { + Transaction transaction(*this); + transaction.Begin(); + + std::string tmp; + fastGetTotalSize_ = + (LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) && + tmp == "1"); + + if (fastGetTotalSize_) + { + currentDiskSize_ = 0; // Unused + } + else + { + // 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(); + } + + transaction.Commit(0); + } + } + + void OrthancPluginDatabase::AddAttachment(int64_t id, const FileInfo& attachment) { @@ -786,52 +871,9 @@ } - class OrthancPluginDatabase::Transaction : public SQLite::ITransaction + IDatabaseWrapper::ITransaction* OrthancPluginDatabase::StartTransaction() { - private: - const OrthancPluginDatabaseBackend& backend_; - void* payload_; - PluginsErrorDictionary& errorDictionary_; - - void CheckSuccess(OrthancPluginErrorCode code) - { - if (code != OrthancPluginErrorCode_Success) - { - errorDictionary_.LogError(code, true); - throw OrthancException(static_cast(code)); - } - } - - public: - Transaction(const OrthancPluginDatabaseBackend& backend, - void* payload, - PluginsErrorDictionary& errorDictionary) : - backend_(backend), - payload_(payload), - errorDictionary_(errorDictionary) - { - } - - virtual void Begin() - { - CheckSuccess(backend_.startTransaction(payload_)); - } - - virtual void Rollback() - { - CheckSuccess(backend_.rollbackTransaction(payload_)); - } - - virtual void Commit() - { - CheckSuccess(backend_.commitTransaction(payload_)); - } - }; - - - SQLite::ITransaction* OrthancPluginDatabase::StartTransaction() - { - return new Transaction(backend_, payload_, errorDictionary_); + return new Transaction(*this); } @@ -892,7 +934,7 @@ { if (extensions_.upgradeDatabase != NULL) { - Transaction transaction(backend_, payload_, errorDictionary_); + Transaction transaction(*this); transaction.Begin(); OrthancPluginErrorCode code = extensions_.upgradeDatabase( @@ -901,7 +943,7 @@ if (code == OrthancPluginErrorCode_Success) { - transaction.Commit(); + transaction.Commit(0); } else { @@ -1104,4 +1146,28 @@ boost::lexical_cast(answer.type)); } } + + + bool OrthancPluginDatabase::IsDiskSizeAbove(uint64_t threshold) + { + if (fastGetTotalSize_) + { + return GetTotalCompressedSize() > threshold; + } + else + { + assert(GetTotalCompressedSize() == currentDiskSize_); + return currentDiskSize_ > threshold; + } + } + + + void OrthancPluginDatabase::ApplyLookupResources(std::vector& patientsId, + std::vector& instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + { + throw OrthancException(ErrorCode_NotImplemented); + } } diff -r ff65c925f57a -r 19ebb606910d Plugins/Engine/OrthancPluginDatabase.h --- a/Plugins/Engine/OrthancPluginDatabase.h Tue Dec 18 14:37:53 2018 +0100 +++ b/Plugins/Engine/OrthancPluginDatabase.h Wed Dec 19 13:10:48 2018 +0100 @@ -57,6 +57,9 @@ void* payload_; IDatabaseListener* listener_; + bool fastGetTotalSize_; + uint64_t currentDiskSize_; + std::list answerStrings_; std::list answerInt32_; std::list answerInt64_; @@ -93,10 +96,7 @@ size_t extensionsSize, void *payload); - virtual void Open() - { - CheckSuccess(backend_.open(payload_)); - } + virtual void Open(); virtual void Close() { @@ -255,7 +255,7 @@ virtual void SetProtectedPatient(int64_t internalId, bool isProtected); - virtual SQLite::ITransaction* StartTransaction(); + virtual IDatabaseWrapper::ITransaction* StartTransaction(); virtual void SetListener(IDatabaseListener& listener) { @@ -268,6 +268,14 @@ IStorageArea& storageArea); void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer); + + virtual bool IsDiskSizeAbove(uint64_t threshold); + + virtual void ApplyLookupResources(std::vector& patientsId, + std::vector& instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit); }; } diff -r ff65c925f57a -r 19ebb606910d UnitTestsSources/DatabaseLookupTests.cpp --- a/UnitTestsSources/DatabaseLookupTests.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/UnitTestsSources/DatabaseLookupTests.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -44,28 +44,19 @@ { { ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, - "HEL*LO", true), OrthancException); + "HEL*LO", true, true), OrthancException); ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, - "HEL?LO", true), OrthancException); + "HEL?LO", true, true), OrthancException); ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, - true), OrthancException); + true, true), OrthancException); - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELLO", true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELLO", true, true); ASSERT_TRUE(tag.IsMatch("HELLO")); ASSERT_FALSE(tag.IsMatch("hello")); ASSERT_TRUE(tag.IsCaseSensitive()); ASSERT_EQ(ConstraintType_Equal, tag.GetConstraintType()); - ASSERT_FALSE(tag.HasTagInfo()); - ASSERT_THROW(tag.GetTagType(), OrthancException); - ASSERT_THROW(tag.GetLevel(), OrthancException); - - tag.SetTagInfo(DicomTagType_Identifier, ResourceType_Series); - ASSERT_TRUE(tag.HasTagInfo()); - ASSERT_EQ(DicomTagType_Identifier, tag.GetTagType()); - ASSERT_EQ(ResourceType_Series, tag.GetLevel()); - DicomMap m; ASSERT_FALSE(tag.IsMatch(m)); m.SetNullValue(DICOM_TAG_PATIENT_NAME); @@ -77,7 +68,7 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELlo", false); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELlo", false, true); ASSERT_TRUE(tag.IsMatch("HELLO")); ASSERT_TRUE(tag.IsMatch("hello")); @@ -85,7 +76,7 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*L?O", true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*L?O", true, true); ASSERT_TRUE(tag.IsMatch("HELLO")); ASSERT_TRUE(tag.IsMatch("HELLLLLO")); ASSERT_TRUE(tag.IsMatch("HELxO")); @@ -93,7 +84,7 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*l?o", false); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*l?o", false, true); ASSERT_TRUE(tag.IsMatch("HELLO")); ASSERT_TRUE(tag.IsMatch("HELLLLLO")); ASSERT_TRUE(tag.IsMatch("HELxO")); @@ -104,21 +95,23 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_SmallerOrEqual, "123", true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_SmallerOrEqual, "123", true, true); ASSERT_TRUE(tag.IsMatch("120")); ASSERT_TRUE(tag.IsMatch("123")); ASSERT_FALSE(tag.IsMatch("124")); + ASSERT_TRUE(tag.IsMandatory()); } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_GreaterOrEqual, "123", true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_GreaterOrEqual, "123", true, false); ASSERT_FALSE(tag.IsMatch("122")); ASSERT_TRUE(tag.IsMatch("123")); ASSERT_TRUE(tag.IsMatch("124")); + ASSERT_FALSE(tag.IsMandatory()); } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, true, true); ASSERT_FALSE(tag.IsMatch("CT")); ASSERT_FALSE(tag.IsMatch("MR")); @@ -137,7 +130,7 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, false); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, false, true); tag.AddValue("ct"); tag.AddValue("mr"); @@ -155,20 +148,16 @@ { { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType()); ASSERT_EQ("HELLO", lookup.GetConstraint(0).GetValue()); ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive()); - - ASSERT_TRUE(lookup.GetConstraint(0).HasTagInfo()); - ASSERT_EQ(DicomTagType_Identifier, lookup.GetConstraint(0).GetTagType()); - ASSERT_EQ(ResourceType_Patient, lookup.GetConstraint(0).GetLevel()); } { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", false); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); // This is *not* a PN VR => "false" above is *not* used @@ -177,14 +166,14 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", true); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", true, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive()); } { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", false); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); // This is a PN VR => "false" above is used @@ -193,11 +182,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false); - - ASSERT_TRUE(lookup.GetConstraint(0).HasTagInfo()); - ASSERT_EQ(DicomTagType_Main, lookup.GetConstraint(0).GetTagType()); - ASSERT_EQ(ResourceType_Series, lookup.GetConstraint(0).GetLevel()); + lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false, true); // This is not a data VR ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType()); @@ -205,7 +190,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-2016", false); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-2016", false, true); // This is a data VR => range is effective ASSERT_EQ(2u, lookup.GetConstraintsCount()); @@ -221,7 +206,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetConstraintType()); @@ -230,7 +215,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "-2016", false); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "-2016", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(DICOM_TAG_PATIENT_BIRTH_DATE, lookup.GetConstraint(0).GetTag()); @@ -240,11 +225,10 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_MODALITIES_IN_STUDY, "CT\\MR", false); + lookup.AddDicomConstraint(DICOM_TAG_MODALITIES_IN_STUDY, "CT\\MR", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(DICOM_TAG_MODALITY, lookup.GetConstraint(0).GetTag()); - ASSERT_EQ(ResourceType_Series, lookup.GetConstraint(0).GetLevel()); ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType()); const std::set& values = lookup.GetConstraint(0).GetValues(); @@ -256,11 +240,10 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "CT\\MR", false); + lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "CT\\MR", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(DICOM_TAG_STUDY_DESCRIPTION, lookup.GetConstraint(0).GetTag()); - ASSERT_EQ(ResourceType_Study, lookup.GetConstraint(0).GetLevel()); ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType()); const std::set& values = lookup.GetConstraint(0).GetValues(); @@ -272,7 +255,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false); + lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType()); @@ -280,7 +263,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false); + lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType()); @@ -288,10 +271,9 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID, "TEST", false); - - ASSERT_TRUE(lookup.GetConstraint(0).HasTagInfo()); - ASSERT_EQ(DicomTagType_Generic, lookup.GetConstraint(0).GetTagType()); - ASSERT_EQ(ResourceType_Instance, lookup.GetConstraint(0).GetLevel()); + lookup.AddDicomConstraint(DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID, "TEST", false, true); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "TEST2", false, false); + ASSERT_TRUE(lookup.GetConstraint(0).IsMandatory()); + ASSERT_FALSE(lookup.GetConstraint(1).IsMandatory()); } } diff -r ff65c925f57a -r 19ebb606910d UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/UnitTestsSources/MultiThreadingTests.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -42,7 +42,7 @@ #include "../Core/SerializationToolbox.h" #include "../Core/SystemToolbox.h" #include "../Core/Toolbox.h" -#include "../OrthancServer/DatabaseWrapper.h" +#include "../OrthancServer/SQLiteDatabaseWrapper.h" #include "../OrthancServer/ServerContext.h" #include "../OrthancServer/ServerJobs/LuaJobManager.h" #include "../OrthancServer/ServerJobs/OrthancJobUnserializer.h" @@ -1281,7 +1281,7 @@ { private: MemoryStorageArea storage_; - DatabaseWrapper db_; // The SQLite DB is in memory + SQLiteDatabaseWrapper db_; // The SQLite DB is in memory std::auto_ptr context_; TimeoutDicomConnectionManager manager_; diff -r ff65c925f57a -r 19ebb606910d UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/UnitTestsSources/ServerIndexTests.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -37,7 +37,7 @@ #include "../Core/FileStorage/FilesystemStorage.h" #include "../Core/FileStorage/MemoryStorageArea.h" #include "../Core/Logging.h" -#include "../OrthancServer/DatabaseWrapper.h" +#include "../OrthancServer/SQLiteDatabaseWrapper.h" #include "../OrthancServer/Search/LookupIdentifierQuery.h" #include "../OrthancServer/ServerContext.h" #include "../OrthancServer/ServerIndex.h" @@ -117,7 +117,7 @@ switch (GetParam()) { case DatabaseWrapperClass_SQLite: - index_.reset(new DatabaseWrapper()); + index_.reset(new SQLiteDatabaseWrapper()); break; default: @@ -141,7 +141,7 @@ { case DatabaseWrapperClass_SQLite: { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); + SQLiteDatabaseWrapper* sqlite = dynamic_cast(index_.get()); ASSERT_EQ(expected, sqlite->GetTableRecordCount(table)); break; } @@ -159,7 +159,7 @@ { case DatabaseWrapperClass_SQLite: { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); + SQLiteDatabaseWrapper* sqlite = dynamic_cast(index_.get()); ASSERT_FALSE(sqlite->GetParentPublicId(s, id)); break; } @@ -177,7 +177,7 @@ { case DatabaseWrapperClass_SQLite: { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); + SQLiteDatabaseWrapper* sqlite = dynamic_cast(index_.get()); ASSERT_TRUE(sqlite->GetParentPublicId(s, id)); ASSERT_EQ(expected, s); break; @@ -196,7 +196,7 @@ { case DatabaseWrapperClass_SQLite: { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); + SQLiteDatabaseWrapper* sqlite = dynamic_cast(index_.get()); sqlite->GetChildren(j, id); ASSERT_EQ(0u, j.size()); break; @@ -215,7 +215,7 @@ { case DatabaseWrapperClass_SQLite: { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); + SQLiteDatabaseWrapper* sqlite = dynamic_cast(index_.get()); sqlite->GetChildren(j, id); ASSERT_EQ(1u, j.size()); ASSERT_EQ(expected, j.front()); @@ -237,7 +237,7 @@ { case DatabaseWrapperClass_SQLite: { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); + SQLiteDatabaseWrapper* sqlite = dynamic_cast(index_.get()); sqlite->GetChildren(j, id); ASSERT_EQ(2u, j.size()); ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) || @@ -464,7 +464,15 @@ CheckTableRecordCount(0, "Resources"); CheckTableRecordCount(0, "AttachedFiles"); - CheckTableRecordCount(2, "GlobalProperties"); + CheckTableRecordCount(3, "GlobalProperties"); + + std::string tmp; + ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion)); + ASSERT_EQ("6", tmp); + ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep)); + ASSERT_EQ("World", tmp); + ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast)); + ASSERT_EQ("1", tmp); ASSERT_EQ(3u, listener_->deletedFiles_.size()); ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), @@ -676,7 +684,7 @@ SystemToolbox::RemoveFile(path + "/index"); FilesystemStorage storage(path); - DatabaseWrapper db; // The SQLite DB is in memory + SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10); context.SetupJobsEngine(true, false); @@ -776,7 +784,7 @@ SystemToolbox::RemoveFile(path + "/index"); FilesystemStorage storage(path); - DatabaseWrapper db; // The SQLite DB is in memory + SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10); context.SetupJobsEngine(true, false); @@ -864,7 +872,7 @@ bool overwrite = (i == 0); MemoryStorageArea storage; - DatabaseWrapper db; // The SQLite DB is in memory + SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10); context.SetupJobsEngine(true, false); diff -r ff65c925f57a -r 19ebb606910d UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Tue Dec 18 14:37:53 2018 +0100 +++ b/UnitTestsSources/UnitTestsMain.cpp Wed Dec 19 13:10:48 2018 +0100 @@ -756,10 +756,29 @@ ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml")); ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml))); ASSERT_THROW(StringToMimeType("nope"), OrthancException); + + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Study)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Instance)); + + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Patient)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Study)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Instance)); + + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Patient)); + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Study)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Instance)); + + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Patient)); + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Study)); + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Instance)); } - #if defined(__linux__) || defined(__OpenBSD__) #include #elif defined(__FreeBSD__)