changeset 1433:461e7554bff7

refactoring: LuaScripting
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 30 Jun 2015 15:09:34 +0200
parents 0ac74fa21db8
children f9cd40166269
files CMakeLists.txt OrthancServer/IServerListener.h OrthancServer/LuaScripting.cpp OrthancServer/LuaScripting.h OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/ServerContext.cpp OrthancServer/ServerContext.h OrthancServer/main.cpp Plugins/Engine/OrthancPlugins.cpp Plugins/Engine/OrthancPlugins.h
diffstat 10 files changed, 539 insertions(+), 275 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Tue Jun 30 12:51:29 2015 +0200
+++ b/CMakeLists.txt	Tue Jun 30 15:09:34 2015 +0200
@@ -173,6 +173,7 @@
   OrthancServer/ResourceFinder.cpp
   OrthancServer/DicomFindQuery.cpp
   OrthancServer/QueryRetrieveHandler.cpp
+  OrthancServer/LuaScripting.cpp
 
   # From "lua-scripting" branch
   OrthancServer/DicomInstanceToStore.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/IServerListener.h	Tue Jun 30 15:09:34 2015 +0200
@@ -0,0 +1,58 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital 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 "DicomInstanceToStore.h"
+#include "ServerIndexChange.h"
+
+#include <json/value.h>
+
+namespace Orthanc
+{
+  class IServerListener
+  {
+  public:
+    virtual ~IServerListener()
+    {
+    }
+
+    virtual void SignalStoredInstance(const std::string& publicId,
+                                      DicomInstanceToStore& instance,
+                                      const Json::Value& simplifiedTags) = 0;                                      
+
+    virtual void SignalChange(const ServerIndexChange& change) = 0;
+
+    virtual bool FilterIncomingInstance(const Json::Value& simplified,
+                                        const std::string& remoteAet) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/LuaScripting.cpp	Tue Jun 30 15:09:34 2015 +0200
@@ -0,0 +1,274 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital 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/>.
+ **/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "LuaScripting.h"
+
+#include "ServerContext.h"
+#include "OrthancInitialization.h"
+#include "../Core/Lua/LuaFunctionCall.h"
+
+#include "Scheduler/DeleteInstanceCommand.h"
+#include "Scheduler/StoreScuCommand.h"
+#include "Scheduler/StorePeerCommand.h"
+#include "Scheduler/ModifyInstanceCommand.h"
+#include "Scheduler/CallSystemCommand.h"
+#include "OrthancRestApi/OrthancRestApi.h"
+
+#include <glog/logging.h>
+#include <EmbeddedResources.h>
+
+static const char* RECEIVED_INSTANCE_FILTER = "ReceivedInstanceFilter";
+static const char* ON_STORED_INSTANCE = "OnStoredInstance";
+
+
+namespace Orthanc
+{
+  IServerCommand* LuaScripting::ParseOperation(const std::string& operation,
+                                               const Json::Value& parameters)
+  {
+    if (operation == "delete")
+    {
+      LOG(INFO) << "Lua script to delete instance " << parameters["Instance"].asString();
+      return new DeleteInstanceCommand(context_);
+    }
+
+    if (operation == "store-scu")
+    {
+      std::string localAet;
+      if (parameters.isMember("LocalAet"))
+      {
+        localAet = parameters["LocalAet"].asString();
+      }
+      else
+      {
+        localAet = context_.GetDefaultLocalApplicationEntityTitle();
+      }
+
+      std::string modality = parameters["Modality"].asString();
+      LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString()
+                << " to modality " << modality << " using Store-SCU";
+      return new StoreScuCommand(context_, localAet,
+                                 Configuration::GetModalityUsingSymbolicName(modality), true);
+    }
+
+    if (operation == "store-peer")
+    {
+      std::string peer = parameters["Peer"].asString();
+      LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString()
+                << " to peer " << peer << " using HTTP";
+
+      OrthancPeerParameters parameters;
+      Configuration::GetOrthancPeer(parameters, peer);
+      return new StorePeerCommand(context_, parameters, true);
+    }
+
+    if (operation == "modify")
+    {
+      LOG(INFO) << "Lua script to modify instance " << parameters["Instance"].asString();
+      DicomModification modification;
+      OrthancRestApi::ParseModifyRequest(modification, parameters);
+
+      std::auto_ptr<ModifyInstanceCommand> command(new ModifyInstanceCommand(context_, modification));
+      return command.release();
+    }
+
+    if (operation == "call-system")
+    {
+      LOG(INFO) << "Lua script to call system command on " << parameters["Instance"].asString();
+
+      const Json::Value& argsIn = parameters["Arguments"];
+      if (argsIn.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadParameterType);
+      }
+
+      std::vector<std::string> args;
+      args.reserve(argsIn.size());
+      for (Json::Value::ArrayIndex i = 0; i < argsIn.size(); ++i)
+      {
+        // http://jsoncpp.sourceforge.net/namespace_json.html#7d654b75c16a57007925868e38212b4e
+        switch (argsIn[i].type())
+        {
+          case Json::stringValue:
+            args.push_back(argsIn[i].asString());
+            break;
+
+          case Json::intValue:
+            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asInt()));
+            break;
+
+          case Json::uintValue:
+            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asUInt()));
+            break;
+
+          case Json::realValue:
+            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asFloat()));
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_BadParameterType);
+        }
+      }
+
+      return new CallSystemCommand(context_, parameters["Command"].asString(), args);
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+
+  LuaScripting::LuaScripting(ServerContext& context) : context_(context)
+  {
+    lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
+    lua_.SetHttpProxy(Configuration::GetGlobalStringParameter("HttpProxy", ""));
+  }
+
+
+  void LuaScripting::ApplyOnStoredInstance(const std::string& instanceId,
+                                           const Json::Value& simplifiedTags,
+                                           const Json::Value& metadata,
+                                           const std::string& remoteAet,
+                                           const std::string& calledAet)
+  {
+    if (lua_.IsExistingFunction(ON_STORED_INSTANCE))
+    {
+      lua_.Execute("_InitializeJob()");
+
+      LuaFunctionCall call(lua_, ON_STORED_INSTANCE);
+      call.PushString(instanceId);
+      call.PushJson(simplifiedTags);
+      call.PushJson(metadata);
+      call.PushJson(remoteAet);
+      call.PushJson(calledAet);
+      call.Execute();
+
+      Json::Value operations;
+      LuaFunctionCall call2(lua_, "_AccessJob");
+      call2.ExecuteToJson(operations);
+     
+      if (operations.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      ServerJob job;
+      ServerCommandInstance* previousCommand = NULL;
+
+      for (Json::Value::ArrayIndex i = 0; i < operations.size(); ++i)
+      {
+        if (operations[i].type() != Json::objectValue ||
+            !operations[i].isMember("Operation"))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        const Json::Value& parameters = operations[i];
+        std::string operation = parameters["Operation"].asString();
+
+        ServerCommandInstance& command = job.AddCommand(ParseOperation(operation, operations[i]));
+        
+        if (!parameters.isMember("Instance"))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        std::string instance = parameters["Instance"].asString();
+        if (instance.empty())
+        {
+          previousCommand->ConnectOutput(command);
+        }
+        else 
+        {
+          command.AddInput(instance);
+        }
+
+        previousCommand = &command;
+      }
+
+      job.SetDescription(std::string("Lua script: ") + ON_STORED_INSTANCE);
+      context_.GetScheduler().Submit(job);
+    }
+  }
+
+
+  void LuaScripting::SignalStoredInstance(const std::string& publicId,
+                                          DicomInstanceToStore& instance,
+                                          const Json::Value& simplifiedTags)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Json::Value metadata = Json::objectValue;
+
+    for (ServerIndex::MetadataMap::const_iterator 
+           it = instance.GetMetadata().begin(); 
+         it != instance.GetMetadata().end(); ++it)
+    {
+      if (it->first.first == ResourceType_Instance)
+      {
+        metadata[EnumerationToString(it->first.second)] = it->second;
+      }
+    }
+
+    ApplyOnStoredInstance(publicId, simplifiedTags, metadata, 
+                          instance.GetRemoteAet(), instance.GetCalledAet());
+  }
+
+
+  void LuaScripting::SignalChange(const ServerIndexChange& change)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // TODO
+  }
+
+
+  bool LuaScripting::FilterIncomingInstance(const Json::Value& simplified,
+                                            const std::string& remoteAet)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (lua_.IsExistingFunction(RECEIVED_INSTANCE_FILTER))
+    {
+      LuaFunctionCall call(lua_, RECEIVED_INSTANCE_FILTER);
+      call.PushJson(simplified);
+      call.PushString(remoteAet);
+
+      if (!call.ExecutePredicate())
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/LuaScripting.h	Tue Jun 30 15:09:34 2015 +0200
@@ -0,0 +1,93 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital 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 "IServerListener.h"
+#include "../Core/Lua/LuaContext.h"
+#include "Scheduler/IServerCommand.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+
+  class LuaScripting : public IServerListener
+  {
+  private:
+    void ApplyOnStoredInstance(const std::string& instanceId,
+                               const Json::Value& simplifiedDicom,
+                               const Json::Value& metadata,
+                               const std::string& remoteAet,
+                               const std::string& calledAet);
+
+    IServerCommand* ParseOperation(const std::string& operation,
+                                   const Json::Value& parameters);
+
+    boost::mutex    mutex_;
+    LuaContext      lua_;
+    ServerContext&  context_;
+
+  public:
+    class Locker : public boost::noncopyable
+    {
+    private:
+      LuaScripting& that_;
+
+    public:
+      Locker(LuaScripting& that) : that_(that)
+      {
+        that.mutex_.lock();
+      }
+
+      ~Locker()
+      {
+        that_.mutex_.unlock();
+      }
+
+      LuaContext& GetLua()
+      {
+        return that_.lua_;
+      }
+    };
+
+    LuaScripting(ServerContext& context);
+
+    virtual void SignalStoredInstance(const std::string& publicId,
+                                      DicomInstanceToStore& instance,
+                                      const Json::Value& simplifiedTags);
+
+    virtual void SignalChange(const ServerIndexChange& change);
+
+    virtual bool FilterIncomingInstance(const Json::Value& simplified,
+                                        const std::string& remoteAet);
+  };
+}
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Tue Jun 30 12:51:29 2015 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Tue Jun 30 15:09:34 2015 +0200
@@ -100,7 +100,7 @@
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     {
-      ServerContext::LuaContextLocker locker(context);
+      LuaScripting::Locker locker(context.GetLua());
       locker.GetLua().Execute(result, call.GetPostBody());
     }
 
--- a/OrthancServer/ServerContext.cpp	Tue Jun 30 12:51:29 2015 +0200
+++ b/OrthancServer/ServerContext.cpp	Tue Jun 30 15:09:34 2015 +0200
@@ -34,7 +34,6 @@
 #include "ServerContext.h"
 
 #include "../Core/HttpServer/FilesystemHttpSender.h"
-#include "../Core/Lua/LuaFunctionCall.h"
 #include "FromDcmtkBridge.h"
 #include "ServerToolbox.h"
 #include "OrthancInitialization.h"
@@ -55,9 +54,6 @@
 
 #define ENABLE_DICOM_CACHE  1
 
-static const char* RECEIVED_INSTANCE_FILTER = "ReceivedInstanceFilter";
-static const char* ON_STORED_INSTANCE = "OnStoredInstance";
-
 static const size_t DICOM_CACHE_SIZE = 2;
 
 /**
@@ -77,6 +73,7 @@
     provider_(*this),
     dicomCache_(provider_, DICOM_CACHE_SIZE),
     scheduler_(Configuration::GetGlobalIntegerParameter("LimitJobs", 10)),
+    lua_(*this),
     plugins_(NULL),
     pluginsManager_(NULL),
     queryRetrieveArchive_(Configuration::GetGlobalIntegerParameter("QueryRetrieveSize", 10)),
@@ -85,8 +82,7 @@
     uint64_t s = Configuration::GetGlobalIntegerParameter("DicomAssociationCloseDelay", 5);  // In seconds
     scu_.SetMillisecondsBeforeClose(s * 1000);  // Milliseconds are expected here
 
-    lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
-    lua_.SetHttpProxy(Configuration::GetGlobalStringParameter("HttpProxy", ""));
+    listeners_.push_back(ServerListener(lua_, "Lua"));
   }
 
   void ServerContext::SetCompressionEnabled(bool enabled)
@@ -106,191 +102,6 @@
   }
 
 
-  bool ServerContext::ApplyReceivedInstanceFilter(const Json::Value& simplified,
-                                                  const std::string& remoteAet)
-  {
-    LuaContextLocker locker(*this);
-
-    if (locker.GetLua().IsExistingFunction(RECEIVED_INSTANCE_FILTER))
-    {
-      LuaFunctionCall call(locker.GetLua(), RECEIVED_INSTANCE_FILTER);
-      call.PushJson(simplified);
-      call.PushString(remoteAet);
-
-      if (!call.ExecutePredicate())
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-
-  static IServerCommand* ParseOperation(ServerContext& context,
-                                        const std::string& operation,
-                                        const Json::Value& parameters)
-  {
-    if (operation == "delete")
-    {
-      LOG(INFO) << "Lua script to delete instance " << parameters["Instance"].asString();
-      return new DeleteInstanceCommand(context);
-    }
-
-    if (operation == "store-scu")
-    {
-      std::string localAet;
-      if (parameters.isMember("LocalAet"))
-      {
-        localAet = parameters["LocalAet"].asString();
-      }
-      else
-      {
-        localAet = context.GetDefaultLocalApplicationEntityTitle();
-      }
-
-      std::string modality = parameters["Modality"].asString();
-      LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString()
-                << " to modality " << modality << " using Store-SCU";
-      return new StoreScuCommand(context, localAet,
-                                 Configuration::GetModalityUsingSymbolicName(modality), true);
-    }
-
-    if (operation == "store-peer")
-    {
-      std::string peer = parameters["Peer"].asString();
-      LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString()
-                << " to peer " << peer << " using HTTP";
-
-      OrthancPeerParameters parameters;
-      Configuration::GetOrthancPeer(parameters, peer);
-      return new StorePeerCommand(context, parameters, true);
-    }
-
-    if (operation == "modify")
-    {
-      LOG(INFO) << "Lua script to modify instance " << parameters["Instance"].asString();
-      DicomModification modification;
-      OrthancRestApi::ParseModifyRequest(modification, parameters);
-
-      std::auto_ptr<ModifyInstanceCommand> command(new ModifyInstanceCommand(context, modification));
-      return command.release();
-    }
-
-    if (operation == "call-system")
-    {
-      LOG(INFO) << "Lua script to call system command on " << parameters["Instance"].asString();
-
-      const Json::Value& argsIn = parameters["Arguments"];
-      if (argsIn.type() != Json::arrayValue)
-      {
-        throw OrthancException(ErrorCode_BadParameterType);
-      }
-
-      std::vector<std::string> args;
-      args.reserve(argsIn.size());
-      for (Json::Value::ArrayIndex i = 0; i < argsIn.size(); ++i)
-      {
-        // http://jsoncpp.sourceforge.net/namespace_json.html#7d654b75c16a57007925868e38212b4e
-        switch (argsIn[i].type())
-        {
-          case Json::stringValue:
-            args.push_back(argsIn[i].asString());
-            break;
-
-          case Json::intValue:
-            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asInt()));
-            break;
-
-          case Json::uintValue:
-            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asUInt()));
-            break;
-
-          case Json::realValue:
-            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asFloat()));
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_BadParameterType);
-        }
-      }
-
-      return new CallSystemCommand(context, parameters["Command"].asString(), args);
-    }
-
-    throw OrthancException(ErrorCode_ParameterOutOfRange);
-  }
-
-
-  void ServerContext::ApplyLuaOnStoredInstance(const std::string& instanceId,
-                                               const Json::Value& simplifiedDicom,
-                                               const Json::Value& metadata,
-                                               const std::string& remoteAet,
-                                               const std::string& calledAet)
-  {
-    LuaContextLocker locker(*this);
-
-    if (locker.GetLua().IsExistingFunction(ON_STORED_INSTANCE))
-    {
-      locker.GetLua().Execute("_InitializeJob()");
-
-      LuaFunctionCall call(locker.GetLua(), ON_STORED_INSTANCE);
-      call.PushString(instanceId);
-      call.PushJson(simplifiedDicom);
-      call.PushJson(metadata);
-      call.PushJson(remoteAet);
-      call.PushJson(calledAet);
-      call.Execute();
-
-      Json::Value operations;
-      LuaFunctionCall call2(locker.GetLua(), "_AccessJob");
-      call2.ExecuteToJson(operations);
-     
-      if (operations.type() != Json::arrayValue)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      ServerJob job;
-      ServerCommandInstance* previousCommand = NULL;
-
-      for (Json::Value::ArrayIndex i = 0; i < operations.size(); ++i)
-      {
-        if (operations[i].type() != Json::objectValue ||
-            !operations[i].isMember("Operation"))
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        const Json::Value& parameters = operations[i];
-        std::string operation = parameters["Operation"].asString();
-
-        ServerCommandInstance& command = job.AddCommand(ParseOperation(*this, operation, operations[i]));
-        
-        if (!parameters.isMember("Instance"))
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        std::string instance = parameters["Instance"].asString();
-        if (instance.empty())
-        {
-          previousCommand->ConnectOutput(command);
-        }
-        else 
-        {
-          command.AddInput(instance);
-        }
-
-        previousCommand = &command;
-      }
-
-      job.SetDescription(std::string("Lua script: ") + ON_STORED_INSTANCE);
-      scheduler_.Submit(job);
-    }
-  }
-
-
   StoreStatus ServerContext::Store(std::string& resultPublicId,
                                    DicomInstanceToStore& dicom)
   {
@@ -303,7 +114,27 @@
       SimplifyTags(simplified, dicom.GetJson());
 
       // Test if the instance must be filtered out
-      if (!ApplyReceivedInstanceFilter(simplified, dicom.GetRemoteAet()))
+      bool accepted = true;
+
+      for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it)
+      {
+        try
+        {
+          if (!it->GetListener().FilterIncomingInstance(simplified, dicom.GetRemoteAet()))
+          {
+            accepted = false;
+            break;
+          }
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Error in the " << it->GetDescription() 
+                     << " callback while receiving an instance: " << e.What();
+          throw;
+        }
+      }
+
+      if (!accepted)
       {
         LOG(INFO) << "An incoming instance has been discarded by the filter";
         return StoreStatus_FilteredOut;
@@ -330,6 +161,7 @@
       StoreStatus status = index_.Store(instanceMetadata, dicom.GetSummary(), attachments, 
                                         dicom.GetRemoteAet(), dicom.GetMetadata());
 
+      // Only keep the metadata for the "instance" level
       dicom.GetMetadata().clear();
 
       for (InstanceMetadata::const_iterator it = instanceMetadata.begin();
@@ -367,33 +199,16 @@
       if (status == StoreStatus_Success ||
           status == StoreStatus_AlreadyStored)
       {
-        Json::Value metadata = Json::objectValue;
-        for (std::map<MetadataType, std::string>::const_iterator 
-               it = instanceMetadata.begin(); 
-             it != instanceMetadata.end(); ++it)
-        {
-          metadata[EnumerationToString(it->first)] = it->second;
-        }
-
-        try
-        {
-          ApplyLuaOnStoredInstance(resultPublicId, simplified, metadata, 
-                                   dicom.GetRemoteAet(), dicom.GetCalledAet());
-        }
-        catch (OrthancException& e)
-        {
-          LOG(ERROR) << "Error in " << ON_STORED_INSTANCE << " callback (Lua): " << e.What();
-        }
-
-        if (plugins_ != NULL)
+        for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it)
         {
           try
           {
-            plugins_->SignalStoredInstance(dicom, resultPublicId);
+            it->GetListener().SignalStoredInstance(resultPublicId, dicom, simplified);
           }
           catch (OrthancException& e)
           {
-            LOG(ERROR) << "Error in " << ON_STORED_INSTANCE << " callback (plugins): " << e.What();
+            LOG(ERROR) << "Error in the " << it->GetDescription() 
+                       << " callback while receiving an instance: " << e.What();
           }
         }
       }
@@ -546,15 +361,16 @@
 
   void ServerContext::SignalChange(const ServerIndexChange& change)
   {
-    if (plugins_ != NULL)
+    for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it)
     {
       try
       {
-        plugins_->SignalChange(change);
+        it->GetListener().SignalChange(change);
       }
       catch (OrthancException& e)
       {
-        LOG(ERROR) << "Error in OnChangeCallback (plugins): " << e.What();
+        LOG(ERROR) << "Error in the " << it->GetDescription() 
+                   << " callback while signaling a change: " << e.What();
       }
     }
   }
--- a/OrthancServer/ServerContext.h	Tue Jun 30 12:51:29 2015 +0200
+++ b/OrthancServer/ServerContext.h	Tue Jun 30 15:09:34 2015 +0200
@@ -33,25 +33,25 @@
 #pragma once
 
 #include "../Core/Cache/MemoryCache.h"
+#include "../Core/Cache/SharedArchive.h"
 #include "../Core/FileStorage/CompressedFileStorageAccessor.h"
 #include "../Core/FileStorage/IStorageArea.h"
+#include "../Core/Lua/LuaContext.h"
 #include "../Core/RestApi/RestApiOutput.h"
-#include "../Core/Lua/LuaContext.h"
-#include "ServerIndex.h"
-#include "ParsedDicomFile.h"
+#include "../Plugins/Engine/OrthancPlugins.h"
+#include "../Plugins/Engine/PluginsManager.h"
+#include "DicomInstanceToStore.h"
 #include "DicomProtocol/ReusableDicomUserConnection.h"
+#include "IServerListener.h"
+#include "LuaScripting.h"
+#include "ParsedDicomFile.h"
 #include "Scheduler/ServerScheduler.h"
-#include "DicomInstanceToStore.h"
-#include "ServerIndexChange.h"
-#include "../Core/Cache/SharedArchive.h"
+#include "ServerIndex.h"
 
 #include <boost/filesystem.hpp>
 
 namespace Orthanc
 {
-  class OrthancPlugins;
-  class PluginsManager;
-
   /**
    * This class is responsible for maintaining the storage area on the
    * filesystem (including compression), as well as the index of the
@@ -73,14 +73,33 @@
       virtual IDynamicObject* Provide(const std::string& id);
     };
 
-    bool ApplyReceivedInstanceFilter(const Json::Value& simplified,
-                                     const std::string& remoteAet);
+    class ServerListener
+    {
+    private:
+      IServerListener *listener_;
+      std::string      description_;
+
+    public:
+      ServerListener(IServerListener& listener,
+                     const std::string& description) :
+        listener_(&listener),
+        description_(description)
+      {
+      }
 
-    void ApplyLuaOnStoredInstance(const std::string& instanceId,
-                                  const Json::Value& simplifiedDicom,
-                                  const Json::Value& metadata,
-                                  const std::string& remoteAet,
-                                  const std::string& calledAet);
+      IServerListener& GetListener()
+      {
+        return *listener_;
+      }
+
+      const std::string& GetDescription()
+      {
+        return description_;
+      }
+    };
+
+    typedef std::list<ServerListener>  ServerListeners;
+
 
     ServerIndex index_;
     CompressedFileStorageAccessor accessor_;
@@ -92,9 +111,9 @@
     ReusableDicomUserConnection scu_;
     ServerScheduler scheduler_;
 
-    boost::mutex luaMutex_;
-    LuaContext lua_;
-    OrthancPlugins* plugins_;  // TODO Turn it into a listener pattern (idem for Lua callbacks)
+    LuaScripting lua_;
+    OrthancPlugins* plugins_;
+    ServerListeners listeners_;
     const PluginsManager* pluginsManager_;
 
     SharedArchive  queryRetrieveArchive_;
@@ -120,29 +139,6 @@
       }
     };
 
-    class LuaContextLocker : public boost::noncopyable
-    {
-    private:
-      ServerContext& that_;
-
-    public:
-      LuaContextLocker(ServerContext& that) : that_(that)
-      {
-        that.luaMutex_.lock();
-      }
-
-      ~LuaContextLocker()
-      {
-        that_.luaMutex_.unlock();
-      }
-
-      LuaContext& GetLua()
-      {
-        return that_.lua_;
-      }
-    };
-
-
     ServerContext(IDatabaseWrapper& database);
 
     void SetStorageArea(IStorageArea& storage)
@@ -208,12 +204,17 @@
     {
       pluginsManager_ = &manager;
       plugins_ = &plugins;
+      listeners_.clear();
+      listeners_.push_back(ServerListener(lua_, "Lua"));  // TODO REFACTOR THIS
+      listeners_.push_back(ServerListener(plugins, "plugin"));  // TODO REFACTOR THIS
     }
 
     void ResetOrthancPlugins()
     {
       pluginsManager_ = NULL;
       plugins_ = NULL;
+      listeners_.clear();
+      listeners_.push_back(ServerListener(lua_, "Lua"));  // TODO REFACTOR THIS
     }
 
     bool DeleteResource(Json::Value& target,
@@ -237,5 +238,10 @@
     {
       return defaultLocalAet_;
     }
+
+    LuaScripting& GetLua()
+    {
+      return lua_;
+    }
   };
 }
--- a/OrthancServer/main.cpp	Tue Jun 30 12:51:29 2015 +0200
+++ b/OrthancServer/main.cpp	Tue Jun 30 15:09:34 2015 +0200
@@ -232,7 +232,7 @@
     {
       std::string lua = "Is" + configuration;
 
-      ServerContext::LuaContextLocker locker(context_);
+      LuaScripting::Locker locker(context_.GetLua());
       
       if (locker.GetLua().IsExistingFunction(lua.c_str()))
       {
@@ -265,7 +265,7 @@
   {
     static const char* HTTP_FILTER = "IncomingHttpRequestFilter";
 
-    ServerContext::LuaContextLocker locker(context_);
+    LuaScripting::Locker locker(context_.GetLua());
 
     // Test if the instance must be filtered out
     if (locker.GetLua().IsExistingFunction(HTTP_FILTER))
@@ -363,7 +363,7 @@
     std::string script;
     Toolbox::ReadFile(script, path);
 
-    ServerContext::LuaContextLocker locker(context);
+    LuaScripting::Locker locker(context.GetLua());
     locker.GetLua().Execute(script);
   }
 }
--- a/Plugins/Engine/OrthancPlugins.cpp	Tue Jun 30 12:51:29 2015 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Tue Jun 30 15:09:34 2015 +0200
@@ -33,13 +33,15 @@
 #include "OrthancPlugins.h"
 
 #include "../../Core/ChunkedBuffer.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/Toolbox.h"
 #include "../../Core/HttpServer/HttpOutput.h"
 #include "../../Core/ImageFormats/PngWriter.h"
-#include "../../OrthancServer/ServerToolbox.h"
+#include "../../Core/MultiThreading/SharedMessageQueue.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
 #include "../../OrthancServer/OrthancInitialization.h"
-#include "../../Core/MultiThreading/SharedMessageQueue.h"
+#include "../../OrthancServer/OrthancRestApi/OrthancRestApi.h"
+#include "../../OrthancServer/ServerContext.h"
+#include "../../OrthancServer/ServerToolbox.h"
 
 #include <boost/thread.hpp>
 #include <boost/regex.hpp> 
@@ -449,8 +451,9 @@
   }
 
 
-  void OrthancPlugins::SignalStoredInstance(DicomInstanceToStore& instance,
-                                            const std::string& instanceId)                                                  
+  void OrthancPlugins::SignalStoredInstance(const std::string& instanceId,
+                                            DicomInstanceToStore& instance,
+                                            const Json::Value& simplifiedTags)
   {
     boost::recursive_mutex::scoped_lock lock(pimpl_->callbackMutex_);
 
--- a/Plugins/Engine/OrthancPlugins.h	Tue Jun 30 12:51:29 2015 +0200
+++ b/Plugins/Engine/OrthancPlugins.h	Tue Jun 30 15:09:34 2015 +0200
@@ -32,18 +32,24 @@
 
 #pragma once
 
-#include "PluginsManager.h"
+#include "../../Core/FileStorage/IStorageArea.h"
 #include "../../Core/HttpServer/HttpHandler.h"
-#include "../../OrthancServer/ServerContext.h"
-#include "../../OrthancServer/OrthancRestApi/OrthancRestApi.h"
+#include "../../OrthancServer/IServerListener.h"
 #include "OrthancPluginDatabase.h"
+#include "PluginsManager.h"
 
 #include <list>
 #include <boost/shared_ptr.hpp>
 
 namespace Orthanc
 {
-  class OrthancPlugins : public HttpHandler, public IPluginServiceProvider
+  class OrthancRestApi;
+  class ServerContext;
+
+  class OrthancPlugins : 
+    public HttpHandler, 
+    public IPluginServiceProvider, 
+    public IServerListener
   {
   private:
     struct PImpl;
@@ -105,10 +111,17 @@
     virtual bool InvokeService(_OrthancPluginService service,
                                const void* parameters);
 
-    void SignalChange(const ServerIndexChange& change);
+    virtual void SignalChange(const ServerIndexChange& change);
+
+    virtual void SignalStoredInstance(const std::string& instanceId,
+                                      DicomInstanceToStore& instance,
+                                      const Json::Value& simplifiedTags);
 
-    void SignalStoredInstance(DicomInstanceToStore& instance,
-                              const std::string& instanceId);
+    virtual bool FilterIncomingInstance(const Json::Value& simplified,
+                                        const std::string& remoteAet)
+    {
+      return true; // TODO Enable filtering of instances from plugins
+    }
 
     void SetOrthancRestApi(OrthancRestApi& restApi);