changeset 1197:61b71ccac362 db-changes

integration mainline->db-changes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 23 Oct 2014 13:19:18 +0200
parents 669bb978d52e (diff) 97089aa85b5f (current diff)
children 1169528a9a5f
files NEWS
diffstat 24 files changed, 713 insertions(+), 117 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Oct 23 13:14:58 2014 +0200
+++ b/CMakeLists.txt	Thu Oct 23 13:19:18 2014 +0200
@@ -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	Thu Oct 23 13:14:58 2014 +0200
+++ b/Core/DicomFormat/DicomTag.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -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	Thu Oct 23 13:14:58 2014 +0200
+++ b/Core/DicomFormat/DicomTag.h	Thu Oct 23 13:19:18 2014 +0200
@@ -86,6 +86,8 @@
 
     static void GetTagsForModule(std::set<DicomTag>& target,
                                  ResourceType module);
+
+    bool IsIdentifier() const;
   };
 
   // Aliases for the most useful tags
--- a/NEWS	Thu Oct 23 13:14:58 2014 +0200
+++ b/NEWS	Thu Oct 23 13:19:18 2014 +0200
@@ -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 21 (Microsoft Visual Studio precompiled headers)
 * Fix issue 22 (Error decoding multi-frame instances)
--- a/OrthancServer/DatabaseWrapper.cpp	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/DatabaseWrapper.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -92,6 +92,34 @@
       }
     };
 
+    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));
+        listener_.SignalChange(ChangeType_Deleted, type, context.GetStringValue(0));
+      }
+    };
+
     class SignalRemainingAncestor : public SQLite::IScalarFunction
     {
     private:
@@ -230,7 +258,7 @@
       throw OrthancException(ErrorCode_InternalError);
     }
 
-    LogChange(changeType, id, type);
+    LogChange(changeType, id, type, publicId);
     return id;
   }
 
@@ -511,18 +539,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 +584,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));
+    }
   }
 
 
@@ -596,14 +650,21 @@
   void DatabaseWrapper::LogChange(ChangeType changeType,
                                   int64_t internalId,
                                   ResourceType resourceType,
-                                  const boost::posix_time::ptime& date)
+                                  const std::string& publicId)
   {
-    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 (changeType <= 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, changeType);
+      s.BindInt64(1, internalId);
+      s.BindInt(2, resourceType);
+      s.BindString(3, boost::posix_time::to_iso_string(now));
+      s.Run();
+    }
+
+    listener_.SignalChange(changeType, resourceType, publicId);
   }
 
 
@@ -852,11 +913,12 @@
       /**
        * History of the database versions:
        *  - 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.3 (inclusive)
+       *  - Version 5: from Orthanc 0.8.4 (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)
       {
@@ -866,6 +928,18 @@
         db_.BeginTransaction();
         db_.Execute(upgrade);
         db_.CommitTransaction();
+        v = 4;
+      }
+
+      if (v == 4)
+      {
+        LOG(WARNING) << "Upgrading database version from 4 to 5";
+        std::string upgrade;
+        EmbeddedResources::GetFileResource(upgrade, EmbeddedResources::UPGRADE_DATABASE_4_TO_5);
+        db_.BeginTransaction();
+        db_.Execute(upgrade);
+        db_.CommitTransaction();
+        v = 5;
       }
     }
     catch (boost::bad_lexical_cast&)
@@ -881,6 +955,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 +1085,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 +1110,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	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/DatabaseWrapper.h	Thu Oct 23 13:19:18 2014 +0200
@@ -157,7 +157,7 @@
     void LogChange(ChangeType changeType,
                    int64_t internalId,
                    ResourceType resourceType,
-                   const boost::posix_time::ptime& date = boost::posix_time::second_clock::local_time());
+                   const std::string& publicId);
 
     void GetChanges(Json::Value& target,
                     int64_t since,
@@ -229,12 +229,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	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/IServerIndexListener.h	Thu Oct 23 13:19:18 2014 +0200
@@ -48,5 +48,9 @@
                                          const std::string& publicId) = 0;
 
     virtual void SignalFileDeleted(const FileInfo& info) = 0;
+
+    virtual void SignalChange(ChangeType changeType,
+                              ResourceType resourceType,
+                              const std::string& publicId) = 0;
   };
 }
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -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	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -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	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.h	Thu Oct 23 13:19:18 2014 +0200
@@ -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	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -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;
     
--- a/OrthancServer/PrepareDatabase.sql	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/PrepareDatabase.sql	Thu Oct 23 13:19:18 2014 +0200
@@ -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	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/ServerContext.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -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,23 @@
   {
     return index_.DeleteResource(target, uuid, expectedType);
   }
+
+
+  void ServerContext::SignalChange(ChangeType changeType,
+                                   ResourceType resourceType,
+                                   const std::string&  publicId)
+  {
+    if (plugins_ != NULL)
+    {
+      try
+      {
+        plugins_->SignalChange(changeType, resourceType, publicId);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Error in OnChangeCallback (plugins): " << e.What();
+      }
+    }
+  }
+
 }
--- a/OrthancServer/ServerContext.h	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/ServerContext.h	Thu Oct 23 13:19:18 2014 +0200
@@ -202,5 +202,9 @@
     bool DeleteResource(Json::Value& target,
                         const std::string& uuid,
                         ResourceType expectedType);
+
+    void SignalChange(ChangeType changeType,
+                      ResourceType resourceType,
+                      const std::string&  publicId);
   };
 }
--- a/OrthancServer/ServerEnumerations.cpp	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/ServerEnumerations.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -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	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/ServerEnumerations.h	Thu Oct 23 13:19:18 2014 +0200
@@ -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	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/ServerIndex.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -61,13 +61,58 @@
     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_;
+        }
+      };
+
+      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_;
+        }
       };
 
       ServerContext& context_;
@@ -75,11 +120,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 +142,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 +160,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->GetChangeType(), it->GetResourceType(), it->GetPublicId());
         }
       }
 
@@ -137,6 +206,23 @@
         sizeOfFilesToRemove_ += info.GetCompressedSize();
       }
 
+      virtual void SignalChange(ChangeType changeType,
+                                ResourceType resourceType,
+                                const std::string& publicId)
+      {
+        LOG(INFO) << "Change related to resource " << publicId << " of type " 
+                  << EnumerationToString(resourceType) << ": " << EnumerationToString(changeType);
+
+        if (insideTransaction_)
+        {
+          pendingChanges_.push_back(ServerIndexChange(changeType, resourceType, publicId));
+        }
+        else
+        {
+          context_.SignalChange(changeType, resourceType, publicId);
+        }
+      }
+
       bool HasRemainingLevel() const
       {
         return hasRemainingLevel_;
@@ -171,9 +257,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 +286,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 +319,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 +337,6 @@
                                    ResourceType expectedType)
   {
     boost::mutex::scoped_lock lock(mutex_);
-    listener_->Reset();
 
     Transaction t(*this);
 
@@ -403,7 +513,6 @@
                                  const MetadataMap& metadata)
   {
     boost::mutex::scoped_lock lock(mutex_);
-    listener_->Reset();
 
     instanceMetadata.clear();
 
@@ -597,13 +706,13 @@
       SeriesStatus seriesStatus = GetSeriesStatus(series);
       if (seriesStatus == SeriesStatus_Complete)
       {
-        db_->LogChange(ChangeType_CompletedSeries, series, ResourceType_Series);
+        db_->LogChange(ChangeType_CompletedSeries, series, 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 +1495,7 @@
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    db_->LogChange(changeType, id, type);
+    db_->LogChange(changeType, id, type, publicId);
 
     transaction->Commit();
   }
@@ -1583,18 +1692,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(ChangeType_StablePatient, id, ResourceType_Patient, payload.GetPublicId());
               break;
 
-            case Orthanc::ResourceType_Study:
-              that->db_->LogChange(ChangeType_StableStudy, id, ResourceType_Study);
+            case ResourceType_Study:
+              that->db_->LogChange(ChangeType_StableStudy, id, ResourceType_Study, payload.GetPublicId());
               break;
 
-            case Orthanc::ResourceType_Series:
-              that->db_->LogChange(ChangeType_StableSeries, id, ResourceType_Series);
+            case ResourceType_Series:
+              that->db_->LogChange(ChangeType_StableSeries, id, ResourceType_Series, payload.GetPublicId());
               break;
 
             default:
@@ -1611,7 +1720,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 +1729,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(ChangeType_NewChildInstance, id, 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 +1761,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 +1780,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 +1849,6 @@
                                      FileContentType type)
   {
     boost::mutex::scoped_lock lock(mutex_);
-    listener_->Reset();
 
     Transaction t(*this);
 
@@ -1780,6 +1892,4 @@
 
     return true;
   }
-
-
 }
--- a/OrthancServer/ServerIndex.h	Thu Oct 23 13:14:58 2014 +0200
+++ b/OrthancServer/ServerIndex.h	Thu Oct 23 13:19:18 2014 +0200
@@ -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/Upgrade4To5.sql	Thu Oct 23 13:19:18 2014 +0200
@@ -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/Plugins/Engine/OrthancPlugins.cpp	Thu Oct 23 13:14:58 2014 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -40,6 +40,7 @@
 #include "../../OrthancServer/ServerToolbox.h"
 #include "../../OrthancServer/OrthancInitialization.h"
 
+#include <boost/thread.hpp>
 #include <boost/regex.hpp> 
 #include <glog/logging.h>
 
@@ -84,13 +85,16 @@
     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_;
 
     PImpl(ServerContext& context) : 
       context_(context), 
@@ -123,6 +127,86 @@
   }
 
 
+  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_AnonymizedPatient:
+        return OrthancPluginChangeType_AnonymizedPatient;
+
+      case ChangeType_AnonymizedSeries:
+        return OrthancPluginChangeType_AnonymizedSeries;
+
+      case ChangeType_AnonymizedStudy:
+        return OrthancPluginChangeType_AnonymizedStudy;
+
+      case ChangeType_CompletedSeries:
+        return OrthancPluginChangeType_CompletedSeries;
+
+      case ChangeType_Deleted:
+        return OrthancPluginChangeType_Deleted;
+
+      case ChangeType_ModifiedPatient:
+        return OrthancPluginChangeType_ModifiedPatient;
+
+      case ChangeType_ModifiedSeries:
+        return OrthancPluginChangeType_ModifiedSeries;
+
+      case ChangeType_ModifiedStudy:
+        return OrthancPluginChangeType_ModifiedStudy;
+
+      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);
+    }
+  }
+
+
   OrthancPlugins::OrthancPlugins(ServerContext& context)
   {
     pimpl_.reset(new PImpl(context));
@@ -257,9 +341,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 +368,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 +383,36 @@
 
 
 
+  void OrthancPlugins::SignalChange(ChangeType changeType,
+                                    ResourceType resourceType,
+                                    const std::string& publicId)
+  {
+    OrthancPluginChangeType c;
+    OrthancPluginResourceType r;
+
+    try
+    {
+      c = Convert(changeType);
+      r = Convert(resourceType);
+    }
+    catch (OrthancException&)
+    {
+      // This change type or resource type is not supported by the plugin SDK
+      return;
+    }
+
+    boost::mutex::scoped_lock lock(pimpl_->callbackMutex_);
+
+    for (PImpl::OnChangeCallbacks::const_iterator
+           callback = pimpl_->onChangeCallbacks_.begin(); 
+         callback != pimpl_->onChangeCallbacks_.end(); ++callback)
+    {
+      (*callback) (c, r, publicId.c_str());
+    }
+  }
+
+
+
   static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
                                  const void* data,
                                  size_t size)
@@ -353,6 +474,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)
   {
@@ -618,7 +749,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 +908,10 @@
         RegisterOnStoredInstanceCallback(parameters);
         return true;
 
+      case _OrthancPluginService_RegisterOnChangeCallback:
+        RegisterOnChangeCallback(parameters);
+        return true;
+
       case _OrthancPluginService_AnswerBuffer:
         AnswerBuffer(parameters);
         return true;
--- a/Plugins/Engine/OrthancPlugins.h	Thu Oct 23 13:14:58 2014 +0200
+++ b/Plugins/Engine/OrthancPlugins.h	Thu Oct 23 13:19:18 2014 +0200
@@ -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,10 @@
     virtual bool InvokeService(_OrthancPluginService service,
                                const void* parameters);
 
+    void SignalChange(ChangeType changeType,
+                      ResourceType resourceType,
+                      const std::string& publicId);
+
     void SignalStoredInstance(DicomInstanceToStore& instance,
                               const std::string& instanceId);
 
--- a/Plugins/OrthancCPlugin/OrthancCPlugin.h	Thu Oct 23 13:14:58 2014 +0200
+++ b/Plugins/OrthancCPlugin/OrthancCPlugin.h	Thu Oct 23 13:19:18 2014 +0200
@@ -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,44 @@
 
 
   /**
+   * 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_AnonymizedPatient = 0,  /*!< Patient resulting from an anomyization */
+    OrthancPluginChangeType_AnonymizedSeries = 1,   /*!< Series resulting from an anonymization */
+    OrthancPluginChangeType_AnonymizedStudy = 2,    /*!< Study resulting from an anomyization */
+    OrthancPluginChangeType_CompletedSeries = 3,    /*!< Series is now complete */
+    OrthancPluginChangeType_Deleted = 4,            /*!< Deleted resource */
+    OrthancPluginChangeType_ModifiedPatient = 5,    /*!< Patient resulting from a modification */
+    OrthancPluginChangeType_ModifiedSeries = 6,     /*!< Series resulting from a modification */
+    OrthancPluginChangeType_ModifiedStudy = 7,      /*!< Study resulting from a modification */
+    OrthancPluginChangeType_NewChildInstance = 8,   /*!< A new instance was added to this resource */
+    OrthancPluginChangeType_NewInstance = 9,        /*!< New instance received */
+    OrthancPluginChangeType_NewPatient = 10,        /*!< New patient created */
+    OrthancPluginChangeType_NewSeries = 11,         /*!< New series created */
+    OrthancPluginChangeType_NewStudy = 12,          /*!< New study created */
+    OrthancPluginChangeType_StablePatient = 13,     /*!< Timeout: No new instance in this patient */
+    OrthancPluginChangeType_StableSeries = 14,      /*!< Timeout: No new instance in this series */
+    OrthancPluginChangeType_StableStudy = 15        /*!< 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 +437,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 +1692,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	Thu Oct 23 13:14:58 2014 +0200
+++ b/Plugins/Samples/Basic/Plugin.c	Thu Oct 23 13:19:18 2014 +0200
@@ -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,16 @@
 }
 
 
+ORTHANC_PLUGINS_API int32_t OnChangeCallback(OrthancPluginChangeType changeType,
+                                             OrthancPluginResourceType resourceType,
+                                             const char* resourceId)
+{
+  char info[1024];
+  sprintf(info, "Change %d on resource %s of type %d", changeType, resourceId, resourceType);
+  OrthancPluginLogWarning(context, info);
+  return 0;
+}
+
 
 ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
 {
@@ -293,6 +309,8 @@
 
   OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback);
 
+  OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
+
   /* Make REST requests to the built-in Orthanc API */
   OrthancPluginRestApiGet(context, &tmp, "/changes");
   OrthancPluginFreeMemoryBuffer(context, &tmp);
--- a/UnitTestsSources/ServerIndexTests.cpp	Thu Oct 23 13:14:58 2014 +0200
+++ b/UnitTestsSources/ServerIndexTests.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -58,6 +58,7 @@
   {
   public:
     std::vector<std::string> deletedFiles_;
+    std::vector<std::string> deletedResources_;
     std::string ancestorId_;
     ResourceType ancestorType_;
 
@@ -79,7 +80,21 @@
       const std::string fileUuid = info.GetUuid();
       deletedFiles_.push_back(fileUuid);
       LOG(INFO) << "A file must be removed: " << fileUuid;
-    }                                
+    }       
+
+    virtual void SignalChange(ChangeType changeType,
+                              ResourceType resourceType,
+                              const std::string& publicId)
+    {
+      if (changeType == ChangeType_Deleted)
+      {
+        deletedResources_.push_back(publicId);        
+      }
+
+      LOG(INFO) << "Change related to resource " << publicId << " of type " 
+                << EnumerationToString(resourceType) << ": " << EnumerationToString(changeType);
+    }
+
   };
 
 
@@ -281,12 +296,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 +317,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 +413,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 +428,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 +504,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 +532,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 +551,7 @@
 
 
 
-TEST_P(DatabaseWrapperTest, LookupTagValue)
+TEST_P(DatabaseWrapperTest, LookupIdentifier)
 {
   int64_t a[] = {
     index_->CreateResource("a", ResourceType_Study),   // 0
@@ -535,29 +568,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;;