# HG changeset patch # User Sebastien Jodogne # Date 1352728350 -3600 # Node ID baada606da3c846cc8d0b4b6912611b2d8acf0db # Parent 93ff5babcaf8073908f72386236d7983bca30d61 databasewrapper diff -r 93ff5babcaf8 -r baada606da3c CMakeLists.txt --- a/CMakeLists.txt Mon Nov 12 10:36:58 2012 +0100 +++ b/CMakeLists.txt Mon Nov 12 14:52:30 2012 +0100 @@ -139,6 +139,7 @@ OrthancServer/OrthancRestApi.cpp OrthancServer/ServerIndex.cpp OrthancServer/ToDcmtkBridge.cpp + OrthancServer/DatabaseWrapper.cpp ) # Ensure autogenerated code is built before building ServerLibrary diff -r 93ff5babcaf8 -r baada606da3c OrthancServer/DatabaseWrapper.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DatabaseWrapper.cpp Mon Nov 12 14:52:30 2012 +0100 @@ -0,0 +1,476 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 Medical Physics Department, CHU of Liege, + * 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 "DatabaseWrapper.h" + +#include "../Core/DicomFormat/DicomArray.h" +#include "EmbeddedResources.h" + +#include +#include + +namespace Orthanc +{ + + namespace Internals + { + class SignalFileDeleted : public SQLite::IScalarFunction + { + private: + IServerIndexListener& listener_; + + public: + SignalFileDeleted(IServerIndexListener& listener) : + listener_(listener) + { + } + + virtual const char* GetName() const + { + return "SignalFileDeleted"; + } + + virtual unsigned int GetCardinality() const + { + return 1; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + listener_.SignalFileDeleted(context.GetStringValue(0)); + } + }; + + class SignalRemainingAncestor : public SQLite::IScalarFunction + { + private: + bool hasRemainingAncestor_; + std::string remainingPublicId_; + ResourceType remainingType_; + + public: + 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::SetGlobalProperty(const std::string& name, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); + s.BindString(0, name); + s.BindString(1, value); + s.Run(); + } + + bool DatabaseWrapper::FindGlobalProperty(std::string& target, + const std::string& name) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM GlobalProperties WHERE name=?"); + s.BindString(0, name); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + return true; + } + } + + std::string DatabaseWrapper::GetGlobalProperty(const std::string& name, + const std::string& defaultValue) + { + std::string s; + if (FindGlobalProperty(s, name)) + { + return s; + } + else + { + return defaultValue; + } + } + + 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::FindResource(const std::string& publicId, + int64_t& id, + ResourceType& type) + { + 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.BindInt(0, parent); + s.BindInt(1, child); + s.Run(); + } + + void DatabaseWrapper::DeleteResource(int64_t id) + { + signalRemainingAncestor_->Reset(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); + s.BindInt(0, id); + s.Run(); + + if (signalRemainingAncestor_->HasRemainingAncestor()) + { + listener_.SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(), + signalRemainingAncestor_->GetRemainingAncestorId()); + } + } + + 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.BindInt(0, id); + s.BindInt(1, type); + s.BindString(2, value); + s.Run(); + } + + bool DatabaseWrapper::FindMetadata(std::string& target, + int64_t id, + MetadataType type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM Metadata WHERE id=? AND type=?"); + s.BindInt(0, id); + s.BindInt(1, type); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + return true; + } + } + + std::string DatabaseWrapper::GetMetadata(int64_t id, + MetadataType type, + const std::string& defaultValue) + { + std::string s; + if (FindMetadata(s, id, type)) + { + return s; + } + else + { + return defaultValue; + } + } + + void DatabaseWrapper::AttachFile(int64_t id, + const std::string& name, + const std::string& fileUuid, + size_t compressedSize, + size_t uncompressedSize, + CompressionType compressionType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?)"); + s.BindInt(0, id); + s.BindString(1, name); + s.BindString(2, fileUuid); + s.BindInt(3, compressedSize); + s.BindInt(4, uncompressedSize); + s.BindInt(5, compressionType); + s.Run(); + } + + bool DatabaseWrapper::FindFile(int64_t id, + const std::string& name, + std::string& fileUuid, + size_t& compressedSize, + size_t& uncompressedSize, + CompressionType& compressionType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT uuid, compressedSize, uncompressedSize, compressionType FROM AttachedFiles WHERE id=? AND name=?"); + s.BindInt(0, id); + s.BindString(1, name); + + if (!s.Step()) + { + return false; + } + else + { + fileUuid = s.ColumnString(0); + compressedSize = s.ColumnInt(1); + uncompressedSize = s.ColumnInt(2); + compressionType = static_cast(s.ColumnInt(3)); + return true; + } + } + + void DatabaseWrapper::SetMainDicomTags(int64_t id, + const DicomMap& tags) + { + DicomArray flattened(tags); + for (size_t i = 0; i < flattened.GetSize(); i++) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); + s.BindInt(0, id); + s.BindInt(1, flattened.GetElement(i).GetTag().GetGroup()); + s.BindInt(2, flattened.GetElement(i).GetTag().GetElement()); + s.BindString(3, flattened.GetElement(i).GetValue().AsString()); + 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.BindInt(0, id); + while (s.Step()) + { + map.SetValue(s.ColumnInt(1), + s.ColumnInt(2), + s.ColumnString(3)); + } + } + + + bool DatabaseWrapper::GetParentPublicId(std::string& result, + 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.BindInt(0, id); + + if (s.Step()) + { + result = s.ColumnString(0); + return true; + } + else + { + return false; + } + } + + + void DatabaseWrapper::GetChildrenPublicId(std::list& result, + 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.BindInt(0, id); + + result.clear(); + + while (s.Step()) + { + result.push_back(s.ColumnString(0)); + } + } + + + void DatabaseWrapper::LogChange(ChangeType changeType, + const std::string& publicId, + ResourceType resourceType, + const boost::posix_time::ptime& date) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); + s.BindInt(0, changeType); + s.BindString(1, publicId); + s.BindInt(2, resourceType); + s.BindString(3, boost::posix_time::to_iso_string(date)); + s.Run(); + } + + + void DatabaseWrapper::LogExportedInstance(const std::string& remoteModality, + DicomInstanceHasher& hasher, + const boost::posix_time::ptime& date) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO ExportedInstances VALUES(NULL, ?, ?, ?, ?, ?, ?)"); + s.BindString(0, remoteModality); + s.BindString(1, hasher.HashInstance()); + s.BindString(2, hasher.GetPatientId()); + s.BindString(3, hasher.GetStudyUid()); + s.BindString(4, hasher.GetSeriesUid()); + s.BindString(5, hasher.GetInstanceUid()); + s.BindString(6, boost::posix_time::to_iso_string(date)); + s.Run(); + } + + + 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); + + assert(s.Step()); + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + + return c; + } + + + 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)); + } + + + DatabaseWrapper::DatabaseWrapper(const std::string& path, + IServerIndexListener& listener) : + listener_(listener) + { + db_.Open(path); + Open(); + } + + DatabaseWrapper::DatabaseWrapper(IServerIndexListener& listener) : + listener_(listener) + { + db_.OpenInMemory(); + Open(); + } + + void DatabaseWrapper::Open() + { + if (!db_.DoesTableExist("GlobalProperties")) + { + LOG(INFO) << "Creating the database"; + std::string query; + EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE_2); + db_.Execute(query); + } + + signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; + db_.Register(signalRemainingAncestor_); + db_.Register(new Internals::SignalFileDeleted(listener_)); + } +} diff -r 93ff5babcaf8 -r baada606da3c OrthancServer/DatabaseWrapper.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DatabaseWrapper.h Mon Nov 12 14:52:30 2012 +0100 @@ -0,0 +1,151 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 Medical Physics Department, CHU of Liege, + * 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 "../Core/SQLite/Connection.h" +#include "../Core/DicomFormat/DicomInstanceHasher.h" +#include "IServerIndexListener.h" + +#include +#include + +namespace Orthanc +{ + namespace Internals + { + class SignalRemainingAncestor; + } + + /** + * This class manages an instance of the Orthanc SQLite database. It + * translates low-level requests into SQL statements. Mutual + * exclusion MUST be implemented at a higher level. + **/ + class DatabaseWrapper + { + private: + IServerIndexListener& listener_; + SQLite::Connection db_; + Internals::SignalRemainingAncestor* signalRemainingAncestor_; + + void Open(); + + public: + void SetGlobalProperty(const std::string& name, + const std::string& value); + + bool FindGlobalProperty(std::string& target, + const std::string& name); + + std::string GetGlobalProperty(const std::string& name, + const std::string& defaultValue = ""); + + int64_t CreateResource(const std::string& publicId, + ResourceType type); + + bool FindResource(const std::string& publicId, + int64_t& id, + ResourceType& type); + + void AttachChild(int64_t parent, + int64_t child); + + void DeleteResource(int64_t id); + + void SetMetadata(int64_t id, + MetadataType type, + const std::string& value); + + bool FindMetadata(std::string& target, + int64_t id, + MetadataType type); + + std::string GetMetadata(int64_t id, + MetadataType type, + const std::string& defaultValue = ""); + + void AttachFile(int64_t id, + const std::string& name, + const std::string& fileUuid, + size_t compressedSize, + size_t uncompressedSize, + CompressionType compressionType); + + void AttachFile(int64_t id, + const std::string& name, + const std::string& fileUuid, + size_t fileSize) + { + AttachFile(id, name, fileUuid, fileSize, fileSize, CompressionType_None); + } + + bool FindFile(int64_t id, + const std::string& name, + std::string& fileUuid, + size_t& compressedSize, + size_t& uncompressedSize, + CompressionType& compressionType); + + void SetMainDicomTags(int64_t id, + const DicomMap& tags); + + void GetMainDicomTags(DicomMap& map, + int64_t id); + + bool GetParentPublicId(std::string& result, + int64_t id); + + void GetChildrenPublicId(std::list& result, + int64_t id); + + void LogChange(ChangeType changeType, + const std::string& publicId, + ResourceType resourceType, + const boost::posix_time::ptime& date); + + void LogExportedInstance(const std::string& remoteModality, + DicomInstanceHasher& hasher, + const boost::posix_time::ptime& date); + + int64_t GetTableRecordCount(const std::string& table); + + uint64_t GetTotalCompressedSize(); + + uint64_t GetTotalUncompressedSize(); + + DatabaseWrapper(const std::string& path, + IServerIndexListener& listener); + + DatabaseWrapper(IServerIndexListener& listener); + }; +} diff -r 93ff5babcaf8 -r baada606da3c OrthancServer/IServerIndexListener.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/IServerIndexListener.h Mon Nov 12 14:52:30 2012 +0100 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 Medical Physics Department, CHU of Liege, + * 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 +#include "ServerEnumerations.h" + +namespace Orthanc +{ + class IServerIndexListener + { + public: + virtual ~IServerIndexListener() + { + } + + virtual void SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) = 0; + + virtual void SignalFileDeleted(const std::string& fileUuid) = 0; + + }; +} diff -r 93ff5babcaf8 -r baada606da3c OrthancServer/PrepareDatabase2.sql --- a/OrthancServer/PrepareDatabase2.sql Mon Nov 12 10:36:58 2012 +0100 +++ b/OrthancServer/PrepareDatabase2.sql Mon Nov 12 14:52:30 2012 +0100 @@ -29,14 +29,39 @@ id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, name TEXT, uuid TEXT, + compressedSize INTEGER, uncompressedSize INTEGER, compressionType INTEGER, PRIMARY KEY(id, name) ); +CREATE TABLE Changes( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + changeType INTEGER, + publicId TEXT, + resourceType INTEGER, + date TEXT + ); + +CREATE TABLE ExportedInstances( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + remoteModality TEXT, + publicId TEXT, + patientId TEXT, + studyInstanceUid TEXT, + seriesInstanceUid TEXT, + sopInstanceUid TEXT, + date TEXT + ); + CREATE INDEX ChildrenIndex ON Resources(parentId); CREATE INDEX PublicIndex ON Resources(publicId); +CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); +CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); +CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); + +CREATE INDEX ChangesIndex ON Changes(publicId); CREATE TRIGGER AttachedFileDeleted AFTER DELETE ON AttachedFiles @@ -47,13 +72,14 @@ CREATE TRIGGER ResourceDeleted AFTER DELETE ON Resources BEGIN - SELECT SignalResourceDeleted(old.resourceType, old.parentId); + SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) + FROM Resources AS parent WHERE internalId = old.parentId; END; - --- -- Delete a resource when its unique child is deleted TODO TODO --- CREATE TRIGGER ResourceRemovedUpward --- AFTER DELETE ON Resources --- FOR EACH ROW --- WHEN (SELECT COUNT(*) FROM ParentRelationship WHERE parent = old. --- END; +-- Delete a parent resource when its unique child is deleted +CREATE TRIGGER ResourceDeletedParentCleaning +AFTER DELETE ON Resources +FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0 +BEGIN + DELETE FROM Resources WHERE internalId = old.parentId; +END; diff -r 93ff5babcaf8 -r baada606da3c OrthancServer/ServerEnumerations.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerEnumerations.h Mon Nov 12 14:52:30 2012 +0100 @@ -0,0 +1,84 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 Medical Physics Department, CHU of Liege, + * 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 + +namespace Orthanc +{ + enum SeriesStatus + { + SeriesStatus_Complete, + SeriesStatus_Missing, + SeriesStatus_Inconsistent, + SeriesStatus_Unknown + }; + + enum StoreStatus + { + StoreStatus_Success, + StoreStatus_AlreadyStored, + StoreStatus_Failure + }; + + + enum ResourceType + { + ResourceType_Patient = 1, + ResourceType_Study = 2, + ResourceType_Series = 3, + ResourceType_Instance = 4 + }; + + enum CompressionType + { + CompressionType_None = 1, + CompressionType_Zlib = 2 + }; + + enum MetadataType + { + MetadataType_Instance_IndexInSeries = 2, + MetadataType_Instance_ReceptionDate = 4, + MetadataType_Instance_RemoteAet = 1, + MetadataType_Series_ExpectedNumberOfInstances = 3 + }; + + enum ChangeType + { + ChangeType_CompletedSeries = 1, + ChangeType_NewInstance = 3, + ChangeType_NewPatient = 4, + ChangeType_NewSeries = 2, + ChangeType_NewStudy = 5, + ChangeType_InvalidSeries = 6 + }; +} diff -r 93ff5babcaf8 -r baada606da3c OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Mon Nov 12 10:36:58 2012 +0100 +++ b/OrthancServer/ServerIndex.h Mon Nov 12 14:52:30 2012 +0100 @@ -37,36 +37,11 @@ #include "../Core/DicomFormat/DicomMap.h" #include "../Core/FileStorage.h" #include "../Core/DicomFormat/DicomInstanceHasher.h" +#include "ServerEnumerations.h" namespace Orthanc { - enum SeriesStatus - { - SeriesStatus_Complete, - SeriesStatus_Missing, - SeriesStatus_Inconsistent, - SeriesStatus_Unknown - }; - - - enum StoreStatus - { - StoreStatus_Success, - StoreStatus_AlreadyStored, - StoreStatus_Failure - }; - - - enum ResourceType - { - ResourceType_Patient = 1, - ResourceType_Study = 2, - ResourceType_Series = 3, - ResourceType_Instance = 4 - }; - - namespace Internals { class SignalDeletedLevelFunction; diff -r 93ff5babcaf8 -r baada606da3c Resources/CMake/BoostConfiguration.cmake --- a/Resources/CMake/BoostConfiguration.cmake Mon Nov 12 10:36:58 2012 +0100 +++ b/Resources/CMake/BoostConfiguration.cmake Mon Nov 12 14:52:30 2012 +0100 @@ -8,7 +8,7 @@ #set(Boost_USE_STATIC_LIBS ON) find_package(Boost - COMPONENTS filesystem thread system) + COMPONENTS filesystem thread system date_time) if (NOT Boost_FOUND) message(FATAL_ERROR "Unable to locate Boost on this system") diff -r 93ff5babcaf8 -r baada606da3c UnitTests/ServerIndex.cpp --- a/UnitTests/ServerIndex.cpp Mon Nov 12 10:36:58 2012 +0100 +++ b/UnitTests/ServerIndex.cpp Mon Nov 12 14:52:30 2012 +0100 @@ -1,471 +1,48 @@ #include "gtest/gtest.h" +#include "../OrthancServer/DatabaseWrapper.h" + #include - -#include "../Core/SQLite/Connection.h" -#include "../Core/Compression/ZlibCompressor.h" -#include "../Core/DicomFormat/DicomTag.h" -#include "../Core/DicomFormat/DicomArray.h" -#include "../Core/FileStorage.h" -#include "../OrthancCppClient/HttpClient.h" -#include "../Core/HttpServer/HttpHandler.h" -#include "../Core/OrthancException.h" -#include "../Core/Toolbox.h" -#include "../Core/Uuid.h" -#include "../OrthancServer/FromDcmtkBridge.h" -#include "../OrthancServer/OrthancInitialization.h" -#include "../OrthancServer/ServerIndex.h" -#include "EmbeddedResources.h" - #include -#include - - -namespace Orthanc -{ - enum CompressionType - { - CompressionType_None = 1, - CompressionType_Zlib = 2 - }; - - enum MetadataType - { - MetadataType_Instance_RemoteAet = 1, - MetadataType_Instance_IndexInSeries = 2, - MetadataType_Series_ExpectedNumberOfInstances = 3 - }; - - class IServerIndexListener - { - public: - virtual ~IServerIndexListener() - { - } - - virtual void SignalResourceDeleted(ResourceType type, - const std::string& parentPublicId) = 0; - - virtual void SignalFileDeleted(const std::string& fileUuid) = 0; - - }; - - namespace Internals - { - class SignalFileDeleted : public SQLite::IScalarFunction - { - private: - IServerIndexListener& listener_; - - public: - SignalFileDeleted(IServerIndexListener& listener) : - listener_(listener) - { - } - - virtual const char* GetName() const - { - return "SignalFileDeleted"; - } - - virtual unsigned int GetCardinality() const - { - return 1; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - listener_.SignalFileDeleted(context.GetStringValue(0)); - } - }; - - class SignalResourceDeleted : public SQLite::IScalarFunction - { - public: - virtual const char* GetName() const - { - return "SignalResourceDeleted"; - } - - virtual unsigned int GetCardinality() const - { - return 2; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - LOG(INFO) << "A resource has been removed, of type " - << context.GetIntValue(0) - << ", with parent " - << context.GetIntValue(1); - } - }; - } - class ServerIndexHelper - { - private: - IServerIndexListener& listener_; - SQLite::Connection db_; - boost::mutex mutex_; - - void Open(const std::string& path); - - public: - void SetGlobalProperty(const std::string& name, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); - s.BindString(0, name); - s.BindString(1, value); - s.Run(); - } - - bool FindGlobalProperty(std::string& target, - const std::string& name) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM GlobalProperties WHERE name=?"); - s.BindString(0, name); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - std::string GetGlobalProperty(const std::string& name, - const std::string& defaultValue = "") - { - std::string s; - if (FindGlobalProperty(s, name)) - { - return s; - } - else - { - return defaultValue; - } - } - - int64_t 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 FindResource(const std::string& publicId, - int64_t& id, - ResourceType& type) - { - 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 AttachChild(int64_t parent, - int64_t child) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); - s.BindInt(0, parent); - s.BindInt(1, child); - s.Run(); - } - - void DeleteResource(int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); - s.BindInt(0, id); - s.Run(); - } - - void SetMetadata(int64_t id, - MetadataType type, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); - s.BindInt(0, id); - s.BindInt(1, type); - s.BindString(2, value); - s.Run(); - } - - bool FindMetadata(std::string& target, - int64_t id, - MetadataType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM Metadata WHERE id=? AND type=?"); - s.BindInt(0, id); - s.BindInt(1, type); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - std::string GetMetadata(int64_t id, - MetadataType type, - const std::string& defaultValue = "") - { - std::string s; - if (FindMetadata(s, id, type)) - { - return s; - } - else - { - return defaultValue; - } - } +using namespace Orthanc; - void AttachFile(int64_t id, - const std::string& name, - const std::string& fileUuid, - size_t uncompressedSize, - CompressionType compressionType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?)"); - s.BindInt(0, id); - s.BindString(1, name); - s.BindString(2, fileUuid); - s.BindInt(3, uncompressedSize); - s.BindInt(4, compressionType); - s.Run(); - } - - bool FindFile(int64_t id, - const std::string& name, - std::string& fileUuid, - size_t& uncompressedSize, - CompressionType& compressionType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT uuid, uncompressedSize, compressionType FROM AttachedFiles WHERE id=? AND name=?"); - s.BindInt(0, id); - s.BindString(1, name); - - if (!s.Step()) - { - return false; - } - else - { - fileUuid = s.ColumnString(0); - uncompressedSize = s.ColumnInt(1); - compressionType = static_cast(s.ColumnInt(2)); - return true; - } - } - - void SetMainDicomTags(int64_t id, - const DicomMap& tags) - { - DicomArray flattened(tags); - for (size_t i = 0; i < flattened.GetSize(); i++) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); - s.BindInt(0, id); - s.BindInt(1, flattened.GetElement(i).GetTag().GetGroup()); - s.BindInt(2, flattened.GetElement(i).GetTag().GetElement()); - s.BindString(3, flattened.GetElement(i).GetValue().AsString()); - s.Run(); - } - } - - void GetMainDicomTags(DicomMap& map, - int64_t id) - { - map.Clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); - s.BindInt(0, id); - while (s.Step()) - { - map.SetValue(s.ColumnInt(1), - s.ColumnInt(2), - s.ColumnString(3)); - } - } - - - bool GetParentPublicId(std::string& result, - 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.BindInt(0, id); - - if (s.Step()) - { - result = s.ColumnString(0); - return true; - } - else - { - return false; - } - } - - - void GetChildrenPublicId(std::list& result, - 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.BindInt(0, id); - - result.clear(); - - while (s.Step()) - { - result.push_back(s.ColumnString(0)); - } - } - - - int64_t GetTableRecordCount(const std::string& table) - { - char buf[128]; - sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str()); - SQLite::Statement s(db_, buf); - - assert(s.Step()); - int64_t c = s.ColumnInt(0); - assert(!s.Step()); - - return c; - } - - ServerIndexHelper(const std::string& path, - IServerIndexListener& listener) : - listener_(listener) - { - Open(path); - } - - ServerIndexHelper(IServerIndexListener& listener) : - listener_(listener) - { - Open(""); - } - }; - - - - void ServerIndexHelper::Open(const std::string& path) - { - if (path == "") - { - db_.OpenInMemory(); - } - else - { - db_.Open(path); - } - - if (!db_.DoesTableExist("GlobalProperties")) - { - LOG(INFO) << "Creating the database"; - std::string query; - EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE_2); - db_.Execute(query); - } - - db_.Register(new Internals::SignalFileDeleted(listener_)); - db_.Register(new Internals::SignalResourceDeleted); - } - - +namespace +{ class ServerIndexListener : public IServerIndexListener { public: - virtual void SignalResourceDeleted(ResourceType type, - const std::string& parentPublicId) + std::set deletedFiles_; + std::string ancestorId_; + ResourceType ancestorType_; + + void Reset() { + ancestorId_ = ""; + deletedFiles_.clear(); + } + + virtual void SignalRemainingAncestor(ResourceType type, + const std::string& publicId) + { + ancestorId_ = publicId; + ancestorType_ = type; } virtual void SignalFileDeleted(const std::string& fileUuid) { + deletedFiles_.insert(fileUuid); LOG(INFO) << "A file must be removed: " << fileUuid; } }; - - /* - class ServerIndex2 - { - private: - ServerIndexListener listener_; - ServerIndexHelper helper_; - - void Open(const std::string& storagePath) - { - boost::filesystem::path p = storagePath; - - try - { - boost::filesystem::create_directories(storagePath); - } - catch (boost::filesystem::filesystem_error) - { - } - - p /= "index"; - } - - public: - ServerIndexHelper(const std::string& storagePath) : - helper_(storagePath) - { - Open(storagePath); - } - }; - */ } - -using namespace Orthanc; - -TEST(ServerIndexHelper, Simple) +TEST(DatabaseWrapper, Simple) { ServerIndexListener listener; - /*Toolbox::RemoveFile("toto"); - ServerIndexHelper index("toto", listener);*/ - ServerIndexHelper index(listener); - - LOG(WARNING) << "ok"; + DatabaseWrapper index(listener); int64_t a[] = { index.CreateResource("a", ResourceType_Patient), // 0 @@ -514,10 +91,13 @@ ASSERT_EQ("e", l.front()); } + index.AttachFile(a[4], "_json", "my json file", 21, 42, CompressionType_Zlib); + index.AttachFile(a[4], "_dicom", "my dicom file", 42); + index.AttachFile(a[6], "_hello", "world", 44); + index.SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE"); - index.AttachFile(a[4], "_json", "my json file", 42, CompressionType_Zlib); - index.AttachFile(a[4], "_dicom", "my dicom file", 42, CompressionType_None); - index.SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE"); + ASSERT_EQ(21 + 42 + 44, index.GetTotalCompressedSize()); + ASSERT_EQ(42 + 42 + 44, index.GetTotalUncompressedSize()); DicomMap m; m.SetValue(0x0010, 0x0010, "PatientName"); @@ -541,23 +121,81 @@ ASSERT_EQ("World", index.GetGlobalProperty("Hello")); ASSERT_EQ("None", index.GetGlobalProperty("Hello2", "None")); - size_t us; + size_t us, cs; CompressionType ct; - ASSERT_TRUE(index.FindFile(a[4], "_json", s, us, ct)); + ASSERT_TRUE(index.FindFile(a[4], "_json", s, cs, us, ct)); ASSERT_EQ("my json file", s); + ASSERT_EQ(21, cs); ASSERT_EQ(42, us); ASSERT_EQ(CompressionType_Zlib, ct); + ASSERT_EQ(0, listener.deletedFiles_.size()); ASSERT_EQ(7, index.GetTableRecordCount("Resources")); - ASSERT_EQ(2, index.GetTableRecordCount("AttachedFiles")); + ASSERT_EQ(3, index.GetTableRecordCount("AttachedFiles")); ASSERT_EQ(1, index.GetTableRecordCount("Metadata")); ASSERT_EQ(1, index.GetTableRecordCount("MainDicomTags")); index.DeleteResource(a[0]); + + ASSERT_EQ(2, listener.deletedFiles_.size()); + ASSERT_NE(listener.deletedFiles_.end(), listener.deletedFiles_.find("my json file")); + ASSERT_NE(listener.deletedFiles_.end(), listener.deletedFiles_.find("my dicom file")); + ASSERT_EQ(2, index.GetTableRecordCount("Resources")); ASSERT_EQ(0, index.GetTableRecordCount("Metadata")); + ASSERT_EQ(1, index.GetTableRecordCount("AttachedFiles")); + ASSERT_EQ(0, index.GetTableRecordCount("MainDicomTags")); + index.DeleteResource(a[5]); + ASSERT_EQ(0, index.GetTableRecordCount("Resources")); ASSERT_EQ(0, index.GetTableRecordCount("AttachedFiles")); - ASSERT_EQ(0, index.GetTableRecordCount("MainDicomTags")); + ASSERT_EQ(1, index.GetTableRecordCount("GlobalProperties")); + + ASSERT_EQ(3, listener.deletedFiles_.size()); + ASSERT_NE(listener.deletedFiles_.end(), listener.deletedFiles_.find("world")); +} + + + + +TEST(DatabaseWrapper, Upward) +{ + ServerIndexListener listener; + DatabaseWrapper index(listener); + + int64_t a[] = { + index.CreateResource("a", ResourceType_Patient), // 0 + index.CreateResource("b", ResourceType_Study), // 1 + index.CreateResource("c", ResourceType_Series), // 2 + index.CreateResource("d", ResourceType_Instance), // 3 + index.CreateResource("e", ResourceType_Instance), // 4 + index.CreateResource("f", ResourceType_Study), // 5 + index.CreateResource("g", ResourceType_Series), // 6 + index.CreateResource("h", ResourceType_Series) // 7 + }; + + index.AttachChild(a[0], a[1]); + index.AttachChild(a[1], a[2]); + index.AttachChild(a[2], a[3]); + index.AttachChild(a[2], a[4]); + index.AttachChild(a[1], a[6]); + index.AttachChild(a[0], a[5]); + index.AttachChild(a[5], a[7]); + + listener.Reset(); + index.DeleteResource(a[3]); + ASSERT_EQ("c", listener.ancestorId_); + ASSERT_EQ(ResourceType_Series, listener.ancestorType_); + + listener.Reset(); + index.DeleteResource(a[4]); + ASSERT_EQ("b", listener.ancestorId_); + ASSERT_EQ(ResourceType_Study, listener.ancestorType_); + + listener.Reset(); + index.DeleteResource(a[7]); + ASSERT_EQ("a", listener.ancestorId_); + ASSERT_EQ(ResourceType_Patient, listener.ancestorType_); + + listener.Reset(); index.DeleteResource(a[6]); - ASSERT_EQ(0, index.GetTableRecordCount("Resources")); - ASSERT_EQ(1, index.GetTableRecordCount("GlobalProperties")); + ASSERT_EQ("", listener.ancestorId_); // No more ancestor }