changeset 1211:88511c737760

integration db-changes->mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 04 Nov 2014 13:57:11 +0100
parents 6502517fd4af (current diff) 178de5edc0a8 (diff)
children 79f868a7f972
files
diffstat 29 files changed, 823 insertions(+), 135 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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);
+  }
 }
--- 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<DicomTag>& target,
                                  ResourceType module);
+
+    bool IsIdentifier() const;
   };
 
   // Aliases for the most useful tags
--- 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_)
--- 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)
--- 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<ResourceType>(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<int64_t>& result,
-                                        DicomTag tag,
-                                        const std::string& value)
+  void  DatabaseWrapper::LookupIdentifier(std::list<int64_t>& 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<int64_t>& result,
-                                        const std::string& value)
+  void  DatabaseWrapper::LookupIdentifier(std::list<int64_t>& 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);
 
--- 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<int64_t>& 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<int64_t>& result,
-                        DicomTag tag,
-                        const std::string& value);
+    void LookupIdentifier(std::list<int64_t>& result,
+                          const DicomTag& tag,
+                          const std::string& value);
 
-    void LookupTagValue(std::list<int64_t>& result,
-                        const std::string& value);
+    void LookupIdentifier(std::list<int64_t>& result,
+                          const std::string& value);
 
     void GetAllMetadata(std::map<MetadataType, std::string>& result,
                         int64_t id);
--- 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 <string>
 #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;
   };
 }
--- 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<std::string> resources;
-        index_.LookupTagValue(resources, tag, value, level_);
+        index_.LookupIdentifier(resources, tag, value, level_);
 
         if (isFilterApplied_)
         {
--- 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<std::string> 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:
--- 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) :
--- 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;
       }
     }
     
--- 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");
--- 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();
+      }
+    }
+  }
 }
--- 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 <boost/filesystem.hpp>
 
@@ -202,5 +203,7 @@
     bool DeleteResource(Json::Value& target,
                         const std::string& uuid,
                         ResourceType expectedType);
+
+    void SignalChange(const ServerIndexChange& change);
   };
 }
--- 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);
     }
--- 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
   };
 
 
--- 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<FileToRemove> pendingFilesToRemove_;
+      std::list<ServerIndexChange> 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<FileToRemove>::iterator 
+        for (std::list<FileToRemove>::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<ServerIndexChange>::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<std::string>& result,
-                                   DicomTag tag,
-                                   const std::string& value,
-                                   ResourceType type)
+  void ServerIndex::LookupIdentifier(std::list<std::string>& result,
+                                     const DicomTag& tag,
+                                     const std::string& value,
+                                     ResourceType type)
   {
     result.clear();
 
     boost::mutex::scoped_lock lock(mutex_);
 
     std::list<int64_t> id;
-    db_->LookupTagValue(id, tag, value);
+    db_->LookupIdentifier(id, tag, value);
 
     for (std::list<int64_t>::const_iterator 
            it = id.begin(); it != id.end(); ++it)
@@ -1648,16 +1728,16 @@
   }
 
 
-  void ServerIndex::LookupTagValue(std::list<std::string>& result,
-                                   DicomTag tag,
-                                   const std::string& value)
+  void ServerIndex::LookupIdentifier(std::list<std::string>& result,
+                                     const DicomTag& tag,
+                                     const std::string& value)
   {
     result.clear();
 
     boost::mutex::scoped_lock lock(mutex_);
 
     std::list<int64_t> id;
-    db_->LookupTagValue(id, tag, value);
+    db_->LookupIdentifier(id, tag, value);
 
     for (std::list<int64_t>::const_iterator 
            it = id.begin(); it != id.end(); ++it)
@@ -1667,15 +1747,15 @@
   }
 
 
-  void ServerIndex::LookupTagValue(std::list< std::pair<ResourceType, std::string> >& result,
-                                   const std::string& value)
+  void ServerIndex::LookupIdentifier(std::list< std::pair<ResourceType, std::string> >& result,
+                                     const std::string& value)
   {
     result.clear();
 
     boost::mutex::scoped_lock lock(mutex_);
 
     std::list<int64_t> id;
-    db_->LookupTagValue(id, value);
+    db_->LookupIdentifier(id, value);
 
     for (std::list<int64_t>::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;
   }
-
-
 }
--- 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<std::string>& result,
-                        DicomTag tag,
-                        const std::string& value,
-                        ResourceType type);
+    void LookupIdentifier(std::list<std::string>& result,
+                          const DicomTag& tag,
+                          const std::string& value,
+                          ResourceType type);
 
-    void LookupTagValue(std::list<std::string>& result,
-                        DicomTag tag,
-                        const std::string& value);
+    void LookupIdentifier(std::list<std::string>& result,
+                          const DicomTag& tag,
+                          const std::string& value);
 
-    void LookupTagValue(std::list< std::pair<ResourceType, std::string> >& result,
-                        const std::string& value);
+    void LookupIdentifier(std::list< std::pair<ResourceType, std::string> >& result,
+                          const std::string& value);
 
     StoreStatus AddAttachment(const FileInfo& attachment,
                               const std::string& publicId);
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ServerEnumerations.h"
+
+#include <string>
+
+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_;
+    }
+  };
+}
--- /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;
--- 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();
--- 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 <boost/thread.hpp>
 #include <boost/regex.hpp> 
 #include <glog/logging.h>
 
 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<OrthancPluginOnChangeCallback>& callbacks)
+      {
+        for (std::list<OrthancPluginOnChangeCallback>::const_iterator 
+               callback = callbacks.begin(); 
+             callback != callbacks.end(); ++callback)
+        {
+          (*callback) (changeType_, resourceType_, publicId_.c_str());
+        }
+      }
+    };
   }
 
 
@@ -84,24 +175,48 @@
     typedef std::pair<boost::regex*, OrthancPluginRestCallback> RestCallback;
     typedef std::list<RestCallback>  RestCallbacks;
     typedef std::list<OrthancPluginOnStoredInstanceCallback>  OnStoredCallbacks;
+    typedef std::list<OrthancPluginOnChangeCallback>  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<IDynamicObject> obj(that->pendingChanges_.Dequeue(500));
+        
+        if (obj.get() != NULL)
+        {
+          boost::mutex::scoped_lock lock(that->callbackMutex_);
+          PendingChange& change = *dynamic_cast<PendingChange*>(obj.get());
+          change.Submit(that->onChangeCallbacks_);
+        }
+      }
+    }
   };
 
 
+  
   static char* CopyString(const std::string& str)
   {
     char *result = reinterpret_cast<char*>(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<const char*>& keys,
                                 std::vector<const char*>& values,
                                 const HttpHandler::Arguments& arguments)
@@ -257,9 +386,14 @@
     }
 
     assert(callback != NULL);
-    int32_t error = callback(reinterpret_cast<OrthancPluginRestOutput*>(&output), 
-                             flatUri.c_str(), 
-                             &request);
+    int32_t error;
+
+    {
+      boost::mutex::scoped_lock lock(pimpl_->callbackMutex_);
+      error = callback(reinterpret_cast<OrthancPluginRestOutput*>(&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<const _OrthancPluginOnChangeCallback*>(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<const _OrthancPluginRetrieveDynamicString*>(parameters);
@@ -618,7 +779,7 @@
     }
 
     std::list<std::string> 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;
--- 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();
   };
 }
--- 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
     {
--- 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, &params);
+  }
+
+
+
+
 #ifdef  __cplusplus
 }
 #endif
--- 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);
--- 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" : [
   ],
--- 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<std::string> deletedFiles_;
+    std::vector<std::string> 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<int64_t> 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<std::string> 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<std::string>::iterator i = s.begin(); i != s.end(); i++)
       {
         std::cout << "*** " << *i << std::endl;;