# HG changeset patch # User Sebastien Jodogne # Date 1415105831 -3600 # Node ID 88511c73776094af4a6a3c2e37b511970669c856 # Parent 6502517fd4af14ff2f88895334104da331c895ee# Parent 178de5edc0a8c63d528e78f002ce05f5a862a068 integration db-changes->mainline diff -r 6502517fd4af -r 88511c737760 CMakeLists.txt --- a/CMakeLists.txt Mon Nov 03 16:45:35 2014 +0100 +++ b/CMakeLists.txt Tue Nov 04 13:57:11 2014 +0100 @@ -264,6 +264,7 @@ set(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 diff -r 6502517fd4af -r 88511c737760 Core/DicomFormat/DicomTag.cpp --- a/Core/DicomFormat/DicomTag.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/Core/DicomFormat/DicomTag.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -244,4 +244,14 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + bool DicomTag::IsIdentifier() const + { + return (*this == DICOM_TAG_PATIENT_ID || + *this == DICOM_TAG_STUDY_INSTANCE_UID || + *this == DICOM_TAG_ACCESSION_NUMBER || + *this == DICOM_TAG_SERIES_INSTANCE_UID || + *this == DICOM_TAG_SOP_INSTANCE_UID); + } } diff -r 6502517fd4af -r 88511c737760 Core/DicomFormat/DicomTag.h --- a/Core/DicomFormat/DicomTag.h Mon Nov 03 16:45:35 2014 +0100 +++ b/Core/DicomFormat/DicomTag.h Tue Nov 04 13:57:11 2014 +0100 @@ -86,6 +86,8 @@ static void GetTagsForModule(std::set& target, ResourceType module); + + bool IsIdentifier() const; }; // Aliases for the most useful tags diff -r 6502517fd4af -r 88511c737760 Core/HttpServer/HttpOutput.cpp --- a/Core/HttpServer/HttpOutput.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/Core/HttpServer/HttpOutput.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -59,7 +59,7 @@ if (state_ != State_Done) { //asm volatile ("int3;"); - LOG(ERROR) << "This HTTP answer does not contain any body"; + //LOG(ERROR) << "This HTTP answer does not contain any body"; } if (hasContentLength_ && contentPosition_ != contentLength_) diff -r 6502517fd4af -r 88511c737760 NEWS --- a/NEWS Mon Nov 03 16:45:35 2014 +0100 +++ b/NEWS Tue Nov 04 13:57:11 2014 +0100 @@ -1,7 +1,9 @@ Pending changes in the mainline =============================== +* Major speed-up thanks to a new database schema * Download ZIP + DICOMDIR from Orthanc Explorer +* Plugins can monitor changes through callbacks * Sample plugin framework to serve static resources * Fix issue 19 (YBR_FULL are decoded incorrectly) * Fix issue 21 (Microsoft Visual Studio precompiled headers) diff -r 6502517fd4af -r 88511c737760 OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/DatabaseWrapper.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -92,6 +92,35 @@ } }; + class SignalResourceDeleted : public SQLite::IScalarFunction + { + private: + IServerIndexListener& listener_; + + public: + SignalResourceDeleted(IServerIndexListener& 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: @@ -230,7 +259,7 @@ throw OrthancException(ErrorCode_InternalError); } - LogChange(changeType, id, type); + LogChange(id, changeType, type, publicId); return id; } @@ -511,18 +540,35 @@ } } + + static void SetMainDicomTagsInternal(SQLite::Statement& s, + int64_t id, + const DicomElement& element) + { + s.BindInt64(0, id); + s.BindInt(1, element.GetTag().GetGroup()); + s.BindInt(2, element.GetTag().GetElement()); + s.BindString(3, element.GetValue().AsString()); + s.Run(); + } + + 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.BindInt64(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(); + if (flattened.GetElement(i).GetTag().IsIdentifier()) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)"); + SetMainDicomTagsInternal(s, id, flattened.GetElement(i)); + } + else + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); + SetMainDicomTagsInternal(s, id, flattened.GetElement(i)); + } } } @@ -539,6 +585,15 @@ s.ColumnInt(2), s.ColumnString(3)); } + + SQLite::Statement s2(db_, SQLITE_FROM_HERE, "SELECT * FROM DicomIdentifiers WHERE id=?"); + s2.BindInt64(0, id); + while (s2.Step()) + { + map.SetValue(s2.ColumnInt(1), + s2.ColumnInt(2), + s2.ColumnString(3)); + } } @@ -593,17 +648,22 @@ } - void DatabaseWrapper::LogChange(ChangeType changeType, - int64_t internalId, - ResourceType resourceType, - const boost::posix_time::ptime& date) + void DatabaseWrapper::LogChange(int64_t internalId, + const ServerIndexChange& change) { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); - s.BindInt(0, changeType); - s.BindInt64(1, internalId); - s.BindInt(2, resourceType); - s.BindString(3, boost::posix_time::to_iso_string(date)); - s.Run(); + if (change.GetChangeType() <= ChangeType_INTERNAL_LastLogged) + { + const boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); + + 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, boost::posix_time::to_iso_string(now)); + s.Run(); + } + + listener_.SignalChange(change); } @@ -807,6 +867,16 @@ } } + static void UpgradeDatabase(SQLite::Connection& db, + EmbeddedResources::FileResourceId script) + { + std::string upgrade; + EmbeddedResources::GetFileResource(upgrade, script); + db.BeginTransaction(); + db.Execute(upgrade); + db.CommitTransaction(); + } + DatabaseWrapper::DatabaseWrapper(const std::string& path, IServerIndexListener& listener) : @@ -851,25 +921,33 @@ /** * History of the database versions: + * - Orthanc before Orthanc 0.3.0 (inclusive) had no version + * - Version 2: only Orthanc 0.3.1 * - Version 3: from Orthanc 0.3.2 to Orthanc 0.7.2 (inclusive) - * - Version 4: from Orthanc 0.7.3 (inclusive) + * - Version 4: from Orthanc 0.7.3 to Orthanc 0.8.4 (inclusive) + * - Version 5: from Orthanc 0.8.5 (inclusive) **/ - // This version of Orthanc is only compatible with versions 3 of 4 of the DB schema - ok = (v == 3 || v == 4); + // This version of Orthanc is only compatible with versions 3, 4 and 5 of the DB schema + ok = (v == 3 || v == 4 || v == 5); if (v == 3) { LOG(WARNING) << "Upgrading database version from 3 to 4"; - std::string upgrade; - EmbeddedResources::GetFileResource(upgrade, EmbeddedResources::UPGRADE_DATABASE_3_TO_4); - db_.BeginTransaction(); - db_.Execute(upgrade); - db_.CommitTransaction(); + UpgradeDatabase(db_, EmbeddedResources::UPGRADE_DATABASE_3_TO_4); + v = 4; + } + + if (v == 4) + { + LOG(WARNING) << "Upgrading database version from 4 to 5"; + UpgradeDatabase(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5); + v = 5; } } catch (boost::bad_lexical_cast&) { + ok = false; } if (!ok) @@ -881,6 +959,7 @@ signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; db_.Register(signalRemainingAncestor_); db_.Register(new Internals::SignalFileDeleted(listener_)); + db_.Register(new Internals::SignalResourceDeleted(listener_)); } uint64_t DatabaseWrapper::GetResourceCount(ResourceType resourceType) @@ -1010,12 +1089,17 @@ } - void DatabaseWrapper::LookupTagValue(std::list& result, - DicomTag tag, - const std::string& value) + void DatabaseWrapper::LookupIdentifier(std::list& result, + const DicomTag& tag, + const std::string& value) { + if (!tag.IsIdentifier()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT id FROM MainDicomTags WHERE tagGroup=? AND tagElement=? and value=?"); + "SELECT id FROM DicomIdentifiers WHERE tagGroup=? AND tagElement=? and value=?"); s.BindInt(0, tag.GetGroup()); s.BindInt(1, tag.GetElement()); @@ -1030,11 +1114,11 @@ } - void DatabaseWrapper::LookupTagValue(std::list& result, - const std::string& value) + void DatabaseWrapper::LookupIdentifier(std::list& result, + const std::string& value) { SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT id FROM MainDicomTags WHERE value=?"); + "SELECT id FROM DicomIdentifiers WHERE value=?"); s.BindString(0, value); diff -r 6502517fd4af -r 88511c737760 OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/DatabaseWrapper.h Tue Nov 04 13:57:11 2014 +0100 @@ -154,10 +154,17 @@ void GetChildrenInternalId(std::list& result, int64_t id); - void LogChange(ChangeType changeType, - int64_t internalId, + void LogChange(int64_t internalId, + ChangeType changeType, ResourceType resourceType, - const boost::posix_time::ptime& date = boost::posix_time::second_clock::local_time()); + const std::string& publicId) + { + ServerIndexChange change(changeType, resourceType, publicId); + LogChange(internalId, change); + } + + void LogChange(int64_t internalId, + const ServerIndexChange& change); void GetChanges(Json::Value& target, int64_t since, @@ -229,12 +236,12 @@ bool IsExistingResource(int64_t internalId); - void LookupTagValue(std::list& result, - DicomTag tag, - const std::string& value); + void LookupIdentifier(std::list& result, + const DicomTag& tag, + const std::string& value); - void LookupTagValue(std::list& result, - const std::string& value); + void LookupIdentifier(std::list& result, + const std::string& value); void GetAllMetadata(std::map& result, int64_t id); diff -r 6502517fd4af -r 88511c737760 OrthancServer/IServerIndexListener.h --- a/OrthancServer/IServerIndexListener.h Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/IServerIndexListener.h Tue Nov 04 13:57:11 2014 +0100 @@ -34,6 +34,7 @@ #include #include "ServerEnumerations.h" +#include "ServerIndexChange.h" namespace Orthanc { @@ -48,5 +49,7 @@ const std::string& publicId) = 0; virtual void SignalFileDeleted(const FileInfo& info) = 0; + + virtual void SignalChange(const ServerIndexChange& change) = 0; }; } diff -r 6502517fd4af -r 88511c737760 OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -320,7 +320,7 @@ << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")"; std::list resources; - index_.LookupTagValue(resources, tag, value, level_); + index_.LookupIdentifier(resources, tag, value, level_); if (isFilterApplied_) { diff -r 6502517fd4af -r 88511c737760 OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -101,7 +101,7 @@ } - bool OrthancMoveRequestHandler::LookupResource(std::string& publicId, + bool OrthancMoveRequestHandler::LookupIdentifier(std::string& publicId, DicomTag tag, const DicomMap& input) { @@ -113,7 +113,7 @@ std::string value = input.GetValue(tag).AsString(); std::list ids; - context_.GetIndex().LookupTagValue(ids, tag, value); + context_.GetIndex().LookupIdentifier(ids, tag, value); if (ids.size() != 1) { @@ -155,19 +155,19 @@ switch (level) { case ResourceType_Patient: - ok = LookupResource(publicId, DICOM_TAG_PATIENT_ID, input); + ok = LookupIdentifier(publicId, DICOM_TAG_PATIENT_ID, input); break; case ResourceType_Study: - ok = LookupResource(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input); + ok = LookupIdentifier(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input); break; case ResourceType_Series: - ok = LookupResource(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input); + ok = LookupIdentifier(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input); break; case ResourceType_Instance: - ok = LookupResource(publicId, DICOM_TAG_SOP_INSTANCE_UID, input); + ok = LookupIdentifier(publicId, DICOM_TAG_SOP_INSTANCE_UID, input); break; default: diff -r 6502517fd4af -r 88511c737760 OrthancServer/OrthancMoveRequestHandler.h --- a/OrthancServer/OrthancMoveRequestHandler.h Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.h Tue Nov 04 13:57:11 2014 +0100 @@ -41,9 +41,9 @@ private: ServerContext& context_; - bool LookupResource(std::string& publicId, - DicomTag tag, - const DicomMap& input); + bool LookupIdentifier(std::string& publicId, + DicomTag tag, + const DicomMap& input); public: OrthancMoveRequestHandler(ServerContext& context) : diff -r 6502517fd4af -r 88511c737760 OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -794,7 +794,7 @@ std::string tag = call.GetPostBody(); Resources resources; - OrthancRestApi::GetIndex(call).LookupTagValue(resources, tag); + OrthancRestApi::GetIndex(call).LookupIdentifier(resources, tag); Json::Value result = Json::arrayValue; @@ -888,7 +888,7 @@ context.GetIndex().GetChildInstances(instances, publicId); // (*) - Json::Value result = Json::arrayValue; + Json::Value result = Json::objectValue; for (Instances::const_iterator it = instances.begin(); it != instances.end(); it++) @@ -900,11 +900,11 @@ { Json::Value simplified; SimplifyTags(simplified, full); - result.append(simplified); + result[*it] = simplified; } else { - result.append(full); + result[*it] = full; } } diff -r 6502517fd4af -r 88511c737760 OrthancServer/PrepareDatabase.sql --- a/OrthancServer/PrepareDatabase.sql Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/PrepareDatabase.sql Tue Nov 04 13:57:11 2014 +0100 @@ -18,6 +18,15 @@ PRIMARY KEY(id, tagGroup, tagElement) ); +-- The following table was added in Orthanc 0.8.5 (database v5) +CREATE TABLE DicomIdentifiers( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + tagGroup INTEGER, + tagElement INTEGER, + value TEXT, + PRIMARY KEY(id, tagGroup, tagElement) + ); + CREATE TABLE Metadata( id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, type INTEGER, @@ -68,8 +77,14 @@ CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); -CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); -CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); +-- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up +-- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); +-- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); + +-- The 3 following indexes were added in Orthanc 0.8.5 (database v5) +CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); +CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); +CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY); CREATE INDEX ChangesIndex ON Changes(internalId); @@ -85,6 +100,7 @@ CREATE TRIGGER ResourceDeleted AFTER DELETE ON Resources BEGIN + SELECT SignalResourceDeleted(old.publicId, old.resourceType); -- New in Orthanc 0.8.5 (db v5) SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) FROM Resources AS parent WHERE internalId = old.parentId; END; @@ -107,4 +123,4 @@ -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration -INSERT INTO GlobalProperties VALUES (1, "4"); +INSERT INTO GlobalProperties VALUES (1, "5"); diff -r 6502517fd4af -r 88511c737760 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/ServerContext.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -371,7 +371,7 @@ } catch (OrthancException& e) { - LOG(ERROR) << "Error in OnStoredInstance callback (Lua): " << e.What(); + LOG(ERROR) << "Error in OnStoredInstance callback (plugins): " << e.What(); } } } @@ -520,4 +520,20 @@ { return index_.DeleteResource(target, uuid, expectedType); } + + + void ServerContext::SignalChange(const ServerIndexChange& change) + { + if (plugins_ != NULL) + { + try + { + plugins_->SignalChange(change); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error in OnChangeCallback (plugins): " << e.What(); + } + } + } } diff -r 6502517fd4af -r 88511c737760 OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/ServerContext.h Tue Nov 04 13:57:11 2014 +0100 @@ -42,6 +42,7 @@ #include "DicomProtocol/ReusableDicomUserConnection.h" #include "Scheduler/ServerScheduler.h" #include "DicomInstanceToStore.h" +#include "ServerIndexChange.h" #include @@ -202,5 +203,7 @@ bool DeleteResource(Json::Value& target, const std::string& uuid, ResourceType expectedType); + + void SignalChange(const ServerIndexChange& change); }; } diff -r 6502517fd4af -r 88511c737760 OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/ServerEnumerations.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -231,6 +231,12 @@ case ChangeType_StableSeries: return "StableSeries"; + case ChangeType_Deleted: + return "Deleted"; + + case ChangeType_NewChildInstance: + return "NewChildInstance"; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } diff -r 6502517fd4af -r 88511c737760 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/ServerEnumerations.h Tue Nov 04 13:57:11 2014 +0100 @@ -134,7 +134,13 @@ ChangeType_ModifiedPatient = 11, ChangeType_StablePatient = 12, ChangeType_StableStudy = 13, - ChangeType_StableSeries = 14 + ChangeType_StableSeries = 14, + + ChangeType_INTERNAL_LastLogged = 4095, + + // The changes below this point are not logged into the database + ChangeType_Deleted = 4096, + ChangeType_NewChildInstance = 4097 }; diff -r 6502517fd4af -r 88511c737760 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/ServerIndex.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -37,6 +37,7 @@ #define NOMINMAX #endif +#include "ServerIndexChange.h" #include "EmbeddedResources.h" #include "OrthancInitialization.h" #include "../Core/Toolbox.h" @@ -61,13 +62,25 @@ private: struct FileToRemove { + private: std::string uuid_; FileContentType type_; + public: FileToRemove(const FileInfo& info) : uuid_(info.GetUuid()), type_(info.GetContentType()) { } + + const std::string& GetUuid() const + { + return uuid_; + } + + FileContentType GetContentType() const + { + return type_; + } }; ServerContext& context_; @@ -75,11 +88,21 @@ ResourceType remainingType_; std::string remainingPublicId_; std::list pendingFilesToRemove_; + std::list pendingChanges_; uint64_t sizeOfFilesToRemove_; + bool insideTransaction_; + + void Reset() + { + sizeOfFilesToRemove_ = 0; + hasRemainingLevel_ = false; + pendingFilesToRemove_.clear(); + pendingChanges_.clear(); + } public: - ServerIndexListener(ServerContext& context) : - context_(context) + ServerIndexListener(ServerContext& context) : context_(context), + insideTransaction_(false) { Reset(); assert(ResourceType_Patient < ResourceType_Study && @@ -87,11 +110,15 @@ ResourceType_Series < ResourceType_Instance); } - void Reset() + void StartTransaction() { - sizeOfFilesToRemove_ = 0; - hasRemainingLevel_ = false; - pendingFilesToRemove_.clear(); + Reset(); + insideTransaction_ = true; + } + + void EndTransaction() + { + insideTransaction_ = false; } uint64_t GetSizeOfFilesToRemove() @@ -101,11 +128,21 @@ void CommitFilesToRemove() { - for (std::list::iterator + for (std::list::const_iterator it = pendingFilesToRemove_.begin(); it != pendingFilesToRemove_.end(); ++it) { - context_.RemoveFile(it->uuid_, it->type_); + context_.RemoveFile(it->GetUuid(), it->GetContentType()); + } + } + + void CommitChanges() + { + for (std::list::const_iterator + it = pendingChanges_.begin(); + it != pendingChanges_.end(); it++) + { + context_.SignalChange(*it); } } @@ -137,6 +174,22 @@ sizeOfFilesToRemove_ += info.GetCompressedSize(); } + virtual void SignalChange(const ServerIndexChange& change) + { + LOG(INFO) << "Change related to resource " << change.GetPublicId() << " of type " + << EnumerationToString(change.GetResourceType()) << ": " + << EnumerationToString(change.GetChangeType()); + + if (insideTransaction_) + { + pendingChanges_.push_back(change); + } + else + { + context_.SignalChange(change); + } + } + bool HasRemainingLevel() const { return hasRemainingLevel_; @@ -171,9 +224,15 @@ { assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize()); - index_.listener_->Reset(); transaction_.reset(index_.db_->StartTransaction()); transaction_->Begin(); + + index_.listener_->StartTransaction(); + } + + ~Transaction() + { + index_.listener_->EndTransaction(); } void Commit(uint64_t sizeOfAddedFiles) @@ -194,22 +253,31 @@ assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize()); + // Send all the pending changes to the Orthanc plugins + index_.listener_->CommitChanges(); + isCommitted_ = true; } } }; - struct ServerIndex::UnstableResourcePayload + class ServerIndex::UnstableResourcePayload { - Orthanc::ResourceType type_; + private: + ResourceType type_; + std::string publicId_; boost::posix_time::ptime time_; - UnstableResourcePayload() : type_(Orthanc::ResourceType_Instance) + public: + UnstableResourcePayload() : type_(ResourceType_Instance) { } - UnstableResourcePayload(Orthanc::ResourceType type) : type_(type) + UnstableResourcePayload(Orthanc::ResourceType type, + const std::string& publicId) : + type_(type), + publicId_(publicId) { time_ = boost::posix_time::second_clock::local_time(); } @@ -218,6 +286,16 @@ { return (boost::posix_time::second_clock::local_time() - time_).total_seconds(); } + + ResourceType GetResourceType() const + { + return type_; + } + + const std::string& GetPublicId() const + { + return publicId_; + } }; @@ -226,7 +304,6 @@ ResourceType expectedType) { boost::mutex::scoped_lock lock(mutex_); - listener_->Reset(); Transaction t(*this); @@ -403,7 +480,6 @@ const MetadataMap& metadata) { boost::mutex::scoped_lock lock(mutex_); - listener_->Reset(); instanceMetadata.clear(); @@ -597,13 +673,13 @@ SeriesStatus seriesStatus = GetSeriesStatus(series); if (seriesStatus == SeriesStatus_Complete) { - db_->LogChange(ChangeType_CompletedSeries, series, ResourceType_Series); + db_->LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, hasher.HashSeries()); } // Mark the parent resources of this instance as unstable - MarkAsUnstable(series, ResourceType_Series); - MarkAsUnstable(study, ResourceType_Study); - MarkAsUnstable(patient, ResourceType_Patient); + MarkAsUnstable(series, ResourceType_Series, hasher.HashSeries()); + MarkAsUnstable(study, ResourceType_Study, hasher.HashStudy()); + MarkAsUnstable(patient, ResourceType_Patient, hasher.HashPatient()); t.Commit(instanceSize); @@ -1386,7 +1462,7 @@ throw OrthancException(ErrorCode_UnknownResource); } - db_->LogChange(changeType, id, type); + db_->LogChange(id, changeType, type, publicId); transaction->Commit(); } @@ -1583,18 +1659,18 @@ // Ensure that the resource is still existing before logging the change if (that->db_->IsExistingResource(id)) { - switch (payload.type_) + switch (payload.GetResourceType()) { - case Orthanc::ResourceType_Patient: - that->db_->LogChange(ChangeType_StablePatient, id, ResourceType_Patient); + case ResourceType_Patient: + that->db_->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId()); break; - case Orthanc::ResourceType_Study: - that->db_->LogChange(ChangeType_StableStudy, id, ResourceType_Study); + case ResourceType_Study: + that->db_->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId()); break; - case Orthanc::ResourceType_Series: - that->db_->LogChange(ChangeType_StableSeries, id, ResourceType_Series); + case ResourceType_Series: + that->db_->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId()); break; default: @@ -1611,7 +1687,8 @@ void ServerIndex::MarkAsUnstable(int64_t id, - Orthanc::ResourceType type) + Orthanc::ResourceType type, + const std::string& publicId) { // WARNING: Before calling this method, "mutex_" must be locked. @@ -1619,23 +1696,26 @@ type == Orthanc::ResourceType_Study || type == Orthanc::ResourceType_Series); - unstableResources_.AddOrMakeMostRecent(id, type); + UnstableResourcePayload payload(type, publicId); + unstableResources_.AddOrMakeMostRecent(id, payload); //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id; + + db_->LogChange(id, ChangeType_NewChildInstance, type, publicId); } - void ServerIndex::LookupTagValue(std::list& result, - DicomTag tag, - const std::string& value, - ResourceType type) + void ServerIndex::LookupIdentifier(std::list& result, + const DicomTag& tag, + const std::string& value, + ResourceType type) { result.clear(); boost::mutex::scoped_lock lock(mutex_); std::list id; - db_->LookupTagValue(id, tag, value); + db_->LookupIdentifier(id, tag, value); for (std::list::const_iterator it = id.begin(); it != id.end(); ++it) @@ -1648,16 +1728,16 @@ } - void ServerIndex::LookupTagValue(std::list& result, - DicomTag tag, - const std::string& value) + void ServerIndex::LookupIdentifier(std::list& result, + const DicomTag& tag, + const std::string& value) { result.clear(); boost::mutex::scoped_lock lock(mutex_); std::list id; - db_->LookupTagValue(id, tag, value); + db_->LookupIdentifier(id, tag, value); for (std::list::const_iterator it = id.begin(); it != id.end(); ++it) @@ -1667,15 +1747,15 @@ } - void ServerIndex::LookupTagValue(std::list< std::pair >& result, - const std::string& value) + void ServerIndex::LookupIdentifier(std::list< std::pair >& result, + const std::string& value) { result.clear(); boost::mutex::scoped_lock lock(mutex_); std::list id; - db_->LookupTagValue(id, value); + db_->LookupIdentifier(id, value); for (std::list::const_iterator it = id.begin(); it != id.end(); ++it) @@ -1736,7 +1816,6 @@ FileContentType type) { boost::mutex::scoped_lock lock(mutex_); - listener_->Reset(); Transaction t(*this); @@ -1780,6 +1859,4 @@ return true; } - - } diff -r 6502517fd4af -r 88511c737760 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/ServerIndex.h Tue Nov 04 13:57:11 2014 +0100 @@ -60,7 +60,7 @@ private: class Transaction; - struct UnstableResourcePayload; + class UnstableResourcePayload; bool done_; boost::mutex mutex_; @@ -92,7 +92,8 @@ void StandaloneRecycling(); void MarkAsUnstable(int64_t id, - Orthanc::ResourceType type); + Orthanc::ResourceType type, + const std::string& publicId); void GetStatisticsInternal(/* out */ uint64_t& compressedSize, /* out */ uint64_t& uncompressedSize, @@ -216,17 +217,17 @@ /* out */ unsigned int& countInstances, const std::string& publicId); - void LookupTagValue(std::list& result, - DicomTag tag, - const std::string& value, - ResourceType type); + void LookupIdentifier(std::list& result, + const DicomTag& tag, + const std::string& value, + ResourceType type); - void LookupTagValue(std::list& result, - DicomTag tag, - const std::string& value); + void LookupIdentifier(std::list& result, + const DicomTag& tag, + const std::string& value); - void LookupTagValue(std::list< std::pair >& result, - const std::string& value); + void LookupIdentifier(std::list< std::pair >& result, + const std::string& value); StoreStatus AddAttachment(const FileInfo& attachment, const std::string& publicId); diff -r 6502517fd4af -r 88511c737760 OrthancServer/ServerIndexChange.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerIndexChange.h Tue Nov 04 13:57:11 2014 +0100 @@ -0,0 +1,73 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 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 "ServerEnumerations.h" + +#include + +namespace Orthanc +{ + struct ServerIndexChange + { + private: + ChangeType changeType_; + ResourceType resourceType_; + std::string publicId_; + + public: + ServerIndexChange(ChangeType changeType, + ResourceType resourceType, + const std::string& publicId) : + changeType_(changeType), + resourceType_(resourceType), + publicId_(publicId) + { + } + + ChangeType GetChangeType() const + { + return changeType_; + } + + ResourceType GetResourceType() const + { + return resourceType_; + } + + const std::string& GetPublicId() const + { + return publicId_; + } + }; +} diff -r 6502517fd4af -r 88511c737760 OrthancServer/Upgrade4To5.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Upgrade4To5.sql Tue Nov 04 13:57:11 2014 +0100 @@ -0,0 +1,66 @@ +-- This SQLite script updates the version of the Orthanc database from 4 to 5. + + +-- Remove 2 indexes to speed up + +DROP INDEX MainDicomTagsIndex2; +DROP INDEX MainDicomTagsIndexValues; + + +-- Add a new table to index the DICOM identifiers + +CREATE TABLE DicomIdentifiers( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + tagGroup INTEGER, + tagElement INTEGER, + value TEXT, + PRIMARY KEY(id, tagGroup, tagElement) + ); + +CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); +CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); +CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY); + + +-- Migrate data from MainDicomTags to MainResourcesTags and MainInstancesTags + +INSERT INTO DicomIdentifiers SELECT * FROM MainDicomTags + WHERE ((tagGroup = 16 AND tagElement = 32) OR -- PatientID (0x0010, 0x0020) + (tagGroup = 32 AND tagElement = 13) OR -- StudyInstanceUID (0x0020, 0x000d) + (tagGroup = 8 AND tagElement = 80) OR -- AccessionNumber (0x0008, 0x0050) + (tagGroup = 32 AND tagElement = 14) OR -- SeriesInstanceUID (0x0020, 0x000e) + (tagGroup = 8 AND tagElement = 24)); -- SOPInstanceUID (0x0008, 0x0018) + +DELETE FROM MainDicomTags + WHERE ((tagGroup = 16 AND tagElement = 32) OR -- PatientID (0x0010, 0x0020) + (tagGroup = 32 AND tagElement = 13) OR -- StudyInstanceUID (0x0020, 0x000d) + (tagGroup = 8 AND tagElement = 80) OR -- AccessionNumber (0x0008, 0x0050) + (tagGroup = 32 AND tagElement = 14) OR -- SeriesInstanceUID (0x0020, 0x000e) + (tagGroup = 8 AND tagElement = 24)); -- SOPInstanceUID (0x0008, 0x0018) + + +-- Upgrade the "ResourceDeleted" trigger + +DROP TRIGGER ResourceDeleted; +DROP TRIGGER ResourceDeletedParentCleaning; + +CREATE TRIGGER ResourceDeleted +AFTER DELETE ON Resources +BEGIN + SELECT SignalResourceDeleted(old.publicId, old.resourceType); + SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) + FROM Resources AS parent WHERE internalId = old.parentId; +END; + +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; + + +-- Change the database version +-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration + +UPDATE GlobalProperties SET value="5" WHERE property=1; diff -r 6502517fd4af -r 88511c737760 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/OrthancServer/main.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -585,8 +585,16 @@ // We're done LOG(WARNING) << "Orthanc is stopping"; +#if ENABLE_PLUGINS == 1 + orthancPlugins.Stop(); + LOG(WARNING) << " Plugins have stopped"; +#endif + dicomServer.Stop(); + LOG(WARNING) << " DICOM server has stopped"; + httpServer.Stop(); + LOG(WARNING) << " HTTP server has stopped"; } serverFactory.Done(); diff -r 6502517fd4af -r 88511c737760 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/Plugins/Engine/OrthancPlugins.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -39,12 +39,76 @@ #include "../../Core/ImageFormats/PngWriter.h" #include "../../OrthancServer/ServerToolbox.h" #include "../../OrthancServer/OrthancInitialization.h" +#include "../../Core/MultiThreading/SharedMessageQueue.h" +#include #include #include namespace Orthanc { + static OrthancPluginResourceType Convert(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return OrthancPluginResourceType_Patient; + + case ResourceType_Study: + return OrthancPluginResourceType_Study; + + case ResourceType_Series: + return OrthancPluginResourceType_Series; + + case ResourceType_Instance: + return OrthancPluginResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static OrthancPluginChangeType Convert(ChangeType type) + { + switch (type) + { + case ChangeType_CompletedSeries: + return OrthancPluginChangeType_CompletedSeries; + + case ChangeType_Deleted: + return OrthancPluginChangeType_Deleted; + + case ChangeType_NewChildInstance: + return OrthancPluginChangeType_NewChildInstance; + + case ChangeType_NewInstance: + return OrthancPluginChangeType_NewInstance; + + case ChangeType_NewPatient: + return OrthancPluginChangeType_NewPatient; + + case ChangeType_NewSeries: + return OrthancPluginChangeType_NewSeries; + + case ChangeType_NewStudy: + return OrthancPluginChangeType_NewStudy; + + case ChangeType_StablePatient: + return OrthancPluginChangeType_StablePatient; + + case ChangeType_StableSeries: + return OrthancPluginChangeType_StableSeries; + + case ChangeType_StableStudy: + return OrthancPluginChangeType_StableStudy; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + namespace { // Anonymous namespace to avoid clashes between compilation modules @@ -75,6 +139,33 @@ } } }; + + + class PendingChange : public IDynamicObject + { + private: + OrthancPluginChangeType changeType_; + OrthancPluginResourceType resourceType_; + std::string publicId_; + + public: + PendingChange(const ServerIndexChange& change) + { + changeType_ = Convert(change.GetChangeType()); + resourceType_ = Convert(change.GetResourceType()); + publicId_ = change.GetPublicId(); + } + + void Submit(std::list& callbacks) + { + for (std::list::const_iterator + callback = callbacks.begin(); + callback != callbacks.end(); ++callback) + { + (*callback) (changeType_, resourceType_, publicId_.c_str()); + } + } + }; } @@ -84,24 +175,48 @@ typedef std::pair RestCallback; typedef std::list RestCallbacks; typedef std::list OnStoredCallbacks; + typedef std::list OnChangeCallbacks; ServerContext& context_; RestCallbacks restCallbacks_; OrthancRestApi* restApi_; OnStoredCallbacks onStoredCallbacks_; + OnChangeCallbacks onChangeCallbacks_; bool hasStorageArea_; _OrthancPluginRegisterStorageArea storageArea_; + boost::mutex callbackMutex_; + SharedMessageQueue pendingChanges_; + boost::thread changeThread_; + bool done_; PImpl(ServerContext& context) : context_(context), restApi_(NULL), - hasStorageArea_(false) + hasStorageArea_(false), + done_(false) { memset(&storageArea_, 0, sizeof(storageArea_)); } + + + static void ChangeThread(PImpl* that) + { + while (!that->done_) + { + std::auto_ptr obj(that->pendingChanges_.Dequeue(500)); + + if (obj.get() != NULL) + { + boost::mutex::scoped_lock lock(that->callbackMutex_); + PendingChange& change = *dynamic_cast(obj.get()); + change.Submit(that->onChangeCallbacks_); + } + } + } }; + static char* CopyString(const std::string& str) { char *result = reinterpret_cast(malloc(str.size() + 1)); @@ -126,11 +241,14 @@ OrthancPlugins::OrthancPlugins(ServerContext& context) { pimpl_.reset(new PImpl(context)); + pimpl_->changeThread_ = boost::thread(PImpl::ChangeThread, pimpl_.get()); } OrthancPlugins::~OrthancPlugins() { + Stop(); + for (PImpl::RestCallbacks::iterator it = pimpl_->restCallbacks_.begin(); it != pimpl_->restCallbacks_.end(); ++it) { @@ -140,6 +258,17 @@ } + void OrthancPlugins::Stop() + { + if (!pimpl_->done_) + { + pimpl_->done_ = true; + pimpl_->changeThread_.join(); + } + } + + + static void ArgumentsToPlugin(std::vector& keys, std::vector& values, const HttpHandler::Arguments& arguments) @@ -257,9 +386,14 @@ } assert(callback != NULL); - int32_t error = callback(reinterpret_cast(&output), - flatUri.c_str(), - &request); + int32_t error; + + { + boost::mutex::scoped_lock lock(pimpl_->callbackMutex_); + error = callback(reinterpret_cast(&output), + flatUri.c_str(), + &request); + } if (error < 0) { @@ -279,8 +413,10 @@ void OrthancPlugins::SignalStoredInstance(DicomInstanceToStore& instance, - const std::string& instanceId) + const std::string& instanceId) { + boost::mutex::scoped_lock lock(pimpl_->callbackMutex_); + for (PImpl::OnStoredCallbacks::const_iterator callback = pimpl_->onStoredCallbacks_.begin(); callback != pimpl_->onStoredCallbacks_.end(); ++callback) @@ -292,6 +428,21 @@ + void OrthancPlugins::SignalChange(const ServerIndexChange& change) + { + try + { + pimpl_->pendingChanges_.Enqueue(new PendingChange(change)); + } + catch (OrthancException&) + { + // This change type or resource type is not supported by the plugin SDK + return; + } + } + + + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, const void* data, size_t size) @@ -353,6 +504,16 @@ } + void OrthancPlugins::RegisterOnChangeCallback(const void* parameters) + { + const _OrthancPluginOnChangeCallback& p = + *reinterpret_cast(parameters); + + LOG(INFO) << "Plugin has registered an OnChange callback"; + pimpl_->onChangeCallbacks_.push_back(p.callback); + } + + void OrthancPlugins::AnswerBuffer(const void* parameters) { @@ -572,7 +733,7 @@ void OrthancPlugins::LookupResource(_OrthancPluginService service, - const void* parameters) + const void* parameters) { const _OrthancPluginRetrieveDynamicString& p = *reinterpret_cast(parameters); @@ -618,7 +779,7 @@ } std::list result; - pimpl_->context_.GetIndex().LookupTagValue(result, tag, p.argument, level); + pimpl_->context_.GetIndex().LookupIdentifier(result, tag, p.argument, level); if (result.size() == 1) { @@ -777,6 +938,10 @@ RegisterOnStoredInstanceCallback(parameters); return true; + case _OrthancPluginService_RegisterOnChangeCallback: + RegisterOnChangeCallback(parameters); + return true; + case _OrthancPluginService_AnswerBuffer: AnswerBuffer(parameters); return true; diff -r 6502517fd4af -r 88511c737760 Plugins/Engine/OrthancPlugins.h --- a/Plugins/Engine/OrthancPlugins.h Mon Nov 03 16:45:35 2014 +0100 +++ b/Plugins/Engine/OrthancPlugins.h Tue Nov 04 13:57:11 2014 +0100 @@ -53,6 +53,8 @@ void RegisterOnStoredInstanceCallback(const void* parameters); + void RegisterOnChangeCallback(const void* parameters); + void AnswerBuffer(const void* parameters); void Redirect(const void* parameters); @@ -95,6 +97,8 @@ virtual bool InvokeService(_OrthancPluginService service, const void* parameters); + void SignalChange(const ServerIndexChange& change); + void SignalStoredInstance(DicomInstanceToStore& instance, const std::string& instanceId); @@ -103,5 +107,7 @@ bool HasStorageArea() const; IStorageArea* GetStorageArea(); + + void Stop(); }; } diff -r 6502517fd4af -r 88511c737760 Plugins/Engine/PluginsManager.cpp --- a/Plugins/Engine/PluginsManager.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/Plugins/Engine/PluginsManager.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -179,7 +179,7 @@ if (error) { - LOG(ERROR) << "Exception when dealing with service " << service; + // LOG(ERROR) << "Exception when dealing with service " << service; } else { diff -r 6502517fd4af -r 88511c737760 Plugins/OrthancCPlugin/OrthancCPlugin.h --- a/Plugins/OrthancCPlugin/OrthancCPlugin.h Mon Nov 03 16:45:35 2014 +0100 +++ b/Plugins/OrthancCPlugin/OrthancCPlugin.h Tue Nov 04 13:57:11 2014 +0100 @@ -27,7 +27,8 @@ * The name and the version of a plugin is only used to prevent it * from being loaded twice. * - * + * The various callbacks are guaranteed to be executed in mutual + * exclusion since Orthanc 0.8.5. **/ @@ -88,7 +89,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 0 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 8 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 3 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 5 @@ -245,6 +246,7 @@ _OrthancPluginService_RegisterRestCallback = 1000, _OrthancPluginService_RegisterOnStoredInstanceCallback = 1001, _OrthancPluginService_RegisterStorageArea = 1002, + _OrthancPluginService_RegisterOnChangeCallback = 1003, /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -341,6 +343,38 @@ /** + * The supported type of DICOM resources. + **/ + typedef enum + { + OrthancPluginResourceType_Patient = 0, /*!< Patient */ + OrthancPluginResourceType_Study = 1, /*!< Study */ + OrthancPluginResourceType_Series = 2, /*!< Series */ + OrthancPluginResourceType_Instance = 3 /*!< Instance */ + } OrthancPluginResourceType; + + + + /** + * The supported type of changes that can happen to DICOM resources. + **/ + typedef enum + { + OrthancPluginChangeType_CompletedSeries = 0, /*!< Series is now complete */ + OrthancPluginChangeType_Deleted = 1, /*!< Deleted resource */ + OrthancPluginChangeType_NewChildInstance = 2, /*!< A new instance was added to this resource */ + OrthancPluginChangeType_NewInstance = 3, /*!< New instance received */ + OrthancPluginChangeType_NewPatient = 4, /*!< New patient created */ + OrthancPluginChangeType_NewSeries = 5, /*!< New series created */ + OrthancPluginChangeType_NewStudy = 6, /*!< New study created */ + OrthancPluginChangeType_StablePatient = 7, /*!< Timeout: No new instance in this patient */ + OrthancPluginChangeType_StableSeries = 8, /*!< Timeout: No new instance in this series */ + OrthancPluginChangeType_StableStudy = 9 /*!< Timeout: No new instance in this study */ + } OrthancPluginChangeType; + + + + /** * @brief A memory buffer allocated by the core system of Orthanc. * * A memory buffer allocated by the core system of Orthanc. When the @@ -397,6 +431,16 @@ /** + * @brief Signature of a callback function that is triggered when a change happens to some DICOM resource. + **/ + typedef int32_t (*OrthancPluginOnChangeCallback) ( + OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId); + + + + /** * @brief Signature of a function to free dynamic memory. **/ typedef void (*OrthancPluginFree) (void* buffer); @@ -1642,6 +1686,33 @@ + typedef struct + { + OrthancPluginOnChangeCallback callback; + } _OrthancPluginOnChangeCallback; + + /** + * @brief Register a callback to monitor changes. + * + * This function registers a callback function that is called + * whenever a change happens to some DICOM resource. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback function. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnChangeCallback( + OrthancPluginContext* context, + OrthancPluginOnChangeCallback callback) + { + _OrthancPluginOnChangeCallback params; + params.callback = callback; + + context->InvokeService(context, _OrthancPluginService_RegisterOnChangeCallback, ¶ms); + } + + + + #ifdef __cplusplus } #endif diff -r 6502517fd4af -r 88511c737760 Plugins/Samples/Basic/Plugin.c --- a/Plugins/Samples/Basic/Plugin.c Mon Nov 03 16:45:35 2014 +0100 +++ b/Plugins/Samples/Basic/Plugin.c Tue Nov 04 13:57:11 2014 +0100 @@ -215,6 +215,7 @@ char buffer[256]; FILE* fp; char* json; + static int first = 1; sprintf(buffer, "Just received a DICOM instance of size %d and ID %s from AET %s", (int) OrthancPluginGetInstanceSize(context, instance), instanceId, @@ -228,7 +229,12 @@ fclose(fp); json = OrthancPluginGetInstanceSimplifiedJson(context, instance); - printf("[%s]\n", json); + if (first) + { + /* Only print the first DICOM instance */ + printf("[%s]\n", json); + first = 0; + } OrthancPluginFreeString(context, json); if (OrthancPluginHasInstanceMetadata(context, instance, "ReceptionDate")) @@ -244,6 +250,31 @@ } +ORTHANC_PLUGINS_API int32_t OnChangeCallback(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId) +{ + char info[1024]; + OrthancPluginMemoryBuffer tmp; + + sprintf(info, "Change %d on resource %s of type %d", changeType, resourceId, resourceType); + OrthancPluginLogWarning(context, info); + + if (changeType == OrthancPluginChangeType_NewInstance) + { + sprintf(info, "/instances/%s/metadata/AnonymizedFrom", resourceId); + if (OrthancPluginRestApiGet(context, &tmp, info) == 0) + { + sprintf(info, " Instance %s comes from the anonymization of instance", resourceId); + strncat(info, (const char*) tmp.data, tmp.size); + OrthancPluginLogWarning(context, info); + OrthancPluginFreeMemoryBuffer(context, &tmp); + } + } + + return 0; +} + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) { @@ -293,6 +324,8 @@ OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback); + OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); + /* Make REST requests to the built-in Orthanc API */ OrthancPluginRestApiGet(context, &tmp, "/changes"); OrthancPluginFreeMemoryBuffer(context, &tmp); diff -r 6502517fd4af -r 88511c737760 Resources/Configuration.json --- a/Resources/Configuration.json Mon Nov 03 16:45:35 2014 +0100 +++ b/Resources/Configuration.json Tue Nov 04 13:57:11 2014 +0100 @@ -34,7 +34,7 @@ ], // List of paths to the plugins that are to be loaded into this - // instance of Orthanc (e.g. "/libPluginTest.so" for Linux, or + // instance of Orthanc (e.g. "./libPluginTest.so" for Linux, or // "./PluginTest.dll" for Windows). "Plugins" : [ ], diff -r 6502517fd4af -r 88511c737760 UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Mon Nov 03 16:45:35 2014 +0100 +++ b/UnitTestsSources/ServerIndexTests.cpp Tue Nov 04 13:57:11 2014 +0100 @@ -58,6 +58,7 @@ { public: std::vector deletedFiles_; + std::vector deletedResources_; std::string ancestorId_; ResourceType ancestorType_; @@ -79,7 +80,20 @@ const std::string fileUuid = info.GetUuid(); deletedFiles_.push_back(fileUuid); LOG(INFO) << "A file must be removed: " << fileUuid; - } + } + + virtual void SignalChange(const ServerIndexChange& change) + { + if (change.GetChangeType() == ChangeType_Deleted) + { + deletedResources_.push_back(change.GetPublicId()); + } + + LOG(INFO) << "Change related to resource " << change.GetPublicId() << " of type " + << EnumerationToString(change.GetResourceType()) << ": " + << EnumerationToString(change.GetChangeType()); + } + }; @@ -281,12 +295,14 @@ ASSERT_EQ(CompressionType_None, att.GetCompressionType()); ASSERT_EQ(0u, listener_->deletedFiles_.size()); + ASSERT_EQ(0u, listener_->deletedResources_.size()); ASSERT_EQ(7u, index_->GetTableRecordCount("Resources")); ASSERT_EQ(3u, index_->GetTableRecordCount("AttachedFiles")); ASSERT_EQ(1u, index_->GetTableRecordCount("Metadata")); ASSERT_EQ(1u, index_->GetTableRecordCount("MainDicomTags")); + index_->DeleteResource(a[0]); - + ASSERT_EQ(5u, listener_->deletedResources_.size()); ASSERT_EQ(2u, listener_->deletedFiles_.size()); ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), listener_->deletedFiles_.end(), @@ -300,6 +316,7 @@ ASSERT_EQ(1u, index_->GetTableRecordCount("AttachedFiles")); ASSERT_EQ(0u, index_->GetTableRecordCount("MainDicomTags")); index_->DeleteResource(a[5]); + ASSERT_EQ(7u, listener_->deletedResources_.size()); ASSERT_EQ(0u, index_->GetTableRecordCount("Resources")); ASSERT_EQ(0u, index_->GetTableRecordCount("AttachedFiles")); ASSERT_EQ(2u, index_->GetTableRecordCount("GlobalProperties")); @@ -395,9 +412,11 @@ ASSERT_EQ(10u, index_->GetTableRecordCount("PatientRecyclingOrder")); listener_->Reset(); + ASSERT_EQ(0u, listener_->deletedResources_.size()); index_->DeleteResource(patients[5]); index_->DeleteResource(patients[0]); + ASSERT_EQ(2u, listener_->deletedResources_.size()); ASSERT_EQ(8u, index_->GetTableRecordCount("Resources")); ASSERT_EQ(8u, index_->GetTableRecordCount("PatientRecyclingOrder")); @@ -408,20 +427,27 @@ int64_t p; ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); index_->DeleteResource(p); + ASSERT_EQ(3u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); index_->DeleteResource(p); + ASSERT_EQ(4u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); index_->DeleteResource(p); + ASSERT_EQ(5u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); index_->DeleteResource(p); + ASSERT_EQ(6u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]); index_->DeleteResource(p); index_->DeleteResource(patients[8]); + ASSERT_EQ(8u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]); index_->DeleteResource(p); + ASSERT_EQ(9u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]); index_->DeleteResource(p); ASSERT_FALSE(index_->SelectPatientToRecycle(p)); + ASSERT_EQ(10u, listener_->deletedResources_.size()); ASSERT_EQ(10u, listener_->deletedFiles_.size()); ASSERT_EQ(0u, index_->GetTableRecordCount("Resources")); @@ -477,16 +503,21 @@ // Unprotecting a patient puts it at the last position in the recycling queue int64_t p; + ASSERT_EQ(0u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]); index_->DeleteResource(p); + ASSERT_EQ(1u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); index_->DeleteResource(p); + ASSERT_EQ(2u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); index_->DeleteResource(p); + ASSERT_EQ(3u, listener_->deletedResources_.size()); ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[2])); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); index_->DeleteResource(p); + ASSERT_EQ(4u, listener_->deletedResources_.size()); // "patients[3]" is still protected ASSERT_FALSE(index_->SelectPatientToRecycle(p)); @@ -500,6 +531,7 @@ ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[2])); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); index_->DeleteResource(p); + ASSERT_EQ(5u, listener_->deletedResources_.size()); ASSERT_EQ(5u, listener_->deletedFiles_.size()); ASSERT_EQ(0u, index_->GetTableRecordCount("Resources")); @@ -518,7 +550,7 @@ -TEST_P(DatabaseWrapperTest, LookupTagValue) +TEST_P(DatabaseWrapperTest, LookupIdentifier) { int64_t a[] = { index_->CreateResource("a", ResourceType_Study), // 0 @@ -535,29 +567,29 @@ std::list s; - index_->LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "0"); + index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "0"); ASSERT_EQ(2u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end()); - index_->LookupTagValue(s, "0"); + index_->LookupIdentifier(s, "0"); ASSERT_EQ(3u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[3]) != s.end()); - index_->LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "1"); + index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1"); ASSERT_EQ(1u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); - index_->LookupTagValue(s, "1"); + index_->LookupIdentifier(s, "1"); ASSERT_EQ(1u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); /*{ std::list s; - context.GetIndex().LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059"); + context.GetIndex().LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059"); for (std::list::iterator i = s.begin(); i != s.end(); i++) { std::cout << "*** " << *i << std::endl;;