changeset 1232:f1c01451a8ee

Introspection of plugins, Plugins can extend Orthanc Explorer with custom JavaScript
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 04 Dec 2014 17:04:40 +0100
parents 703fcd797186
children eac00401cb96
files NEWS OrthancExplorer/explorer-plugin.js OrthancExplorer/explorer.html OrthancExplorer/explorer.js OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/ServerContext.cpp OrthancServer/ServerContext.h OrthancServer/main.cpp Plugins/Engine/OrthancPlugins.cpp Plugins/Engine/OrthancPlugins.h Plugins/Engine/PluginsManager.cpp Plugins/Engine/PluginsManager.h Plugins/OrthancCPlugin/OrthancCPlugin.h Plugins/Samples/Basic/Plugin.c Resources/OrthancPlugin.doxygen
diffstat 15 files changed, 392 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Nov 28 12:39:22 2014 +0100
+++ b/NEWS	Thu Dec 04 17:04:40 2014 +0100
@@ -1,6 +1,8 @@
 Pending changes in the mainline
 ===============================
 
+* Introspection of plugins
+* Plugins can extend Orthanc Explorer with custom JavaScript
 * Instances without PatientID are now allowed
 * Support of Tudor DICOM in Query/Retrieve
 
--- a/OrthancExplorer/explorer-plugin.js	Fri Nov 28 12:39:22 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-// This file can be overridden by some plugin to adapt the default Orthanc Explorer
--- a/OrthancExplorer/explorer.html	Fri Nov 28 12:39:22 2014 +0100
+++ b/OrthancExplorer/explorer.html	Thu Dec 04 17:04:40 2014 +0100
@@ -30,12 +30,13 @@
     <link rel="stylesheet" href="explorer.css" />
     <script src="file-upload.js"></script>
     <script src="explorer.js"></script>
-    <script src="explorer-plugin.js"></script>
+    <script src="../plugins/explorer.js"></script>
   </head>
   <body>
     <div data-role="page" id="find-patients" >
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Find a patient</h1>
+        <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -47,7 +48,7 @@
     <div data-role="page" id="upload" >
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Upload DICOM files</h1>
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
       </div>
       <div data-role="content">
         <div style="display:none">
@@ -73,7 +74,7 @@
     <div data-role="page" id="patient" >
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Patient</h1>
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -127,7 +128,7 @@
           <a href="#" class="patient-link">Patient</a> &raquo; 
           Study
         </h1>
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -176,7 +177,7 @@
           Series
         </h1>
 
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -226,7 +227,7 @@
           <a href="#" class="series-link">Series</a> &raquo; 
           Instance
         </h1>
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -272,6 +273,18 @@
       </div>
     </div>
 
+    <div data-role="page" id="plugins" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>Plugins</h1>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+      </div>
+      <div data-role="content">
+        <ul id="all-plugins" data-role="listview" data-inset="true" data-filter="true">
+        </ul>
+      </div>
+    </div>
+
+
     <div id="peer-store" style="display:none;" class="ui-body-c">
       <p align="center"><b>Sending to Orthanc peer...</b></p>
       <p><img src="libs/images/ajax-loader2.gif" alt="" /></p>
--- a/OrthancExplorer/explorer.js	Fri Nov 28 12:39:22 2014 +0100
+++ b/OrthancExplorer/explorer.js	Thu Dec 04 17:04:40 2014 +0100
@@ -1023,3 +1023,46 @@
   OpenAnonymizeResourceDialog('../patients/' + $.mobile.pageData.uuid,
                               'Anonymize this patient?');
 });
+
+
+$('#plugins').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../plugins',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(plugins) {
+      var target = $('#all-plugins');
+      $('li', target).remove();
+
+      plugins.map(function(id) {
+        return $.ajax({
+          url: '../plugins/' + id,
+          dataType: 'json',
+          async: false,
+          cache: false,
+          success: function(plugin) {
+            var li = $('<li>');
+            var item = li;
+
+            if ('RootUri' in plugin)
+            {
+              item = $('<a>');
+              li.append(item);
+              item.click(function() {
+                window.open(plugin.RootUri);
+              });
+            }
+
+            item.append($('<h1>').text(plugin.ID));
+            item.append($('<p>').text(plugin.Description));
+            item.append($('<span>').addClass('ui-li-count').text(plugin.Version));
+            target.append(li);
+          }
+        });
+      });
+
+      target.listview('refresh');
+    }
+  });
+});
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Fri Nov 28 12:39:22 2014 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Thu Dec 04 17:04:40 2014 +0100
@@ -35,6 +35,8 @@
 
 #include "../OrthancInitialization.h"
 #include "../FromDcmtkBridge.h"
+#include "../../Plugins/Engine/PluginsManager.h"
+#include "../../Plugins/Engine/OrthancPlugins.h"
 
 #include <glog/logging.h>
 
@@ -114,6 +116,93 @@
   }
 
 
+  // Plugins information ------------------------------------------------------
+
+  static void ListPlugins(RestApiGetCall& call)
+  {
+    Json::Value v = Json::arrayValue;
+
+    if (OrthancRestApi::GetContext(call).HasPlugins())
+    {
+      std::list<std::string> plugins;
+      OrthancRestApi::GetContext(call).GetPluginsManager().ListPlugins(plugins);
+
+      for (std::list<std::string>::const_iterator 
+             it = plugins.begin(); it != plugins.end(); it++)
+      {
+        v.append(*it);
+      }
+    }
+
+    call.GetOutput().AnswerJson(v);
+  }
+
+
+  static void GetPlugin(RestApiGetCall& call)
+  {
+    if (!OrthancRestApi::GetContext(call).HasPlugins())
+    {
+      return;
+    }
+
+    const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPluginsManager();
+    std::string id = call.GetUriComponent("id", "");
+
+    if (manager.HasPlugin(id))
+    {
+      Json::Value v = Json::objectValue;
+      v["ID"] = id;
+      v["Version"] = manager.GetPluginVersion(id);
+
+      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetOrthancPlugins();
+      const char *c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_RootUri);
+      if (c != NULL)
+      {
+        v["RootUri"] = c;
+      }
+
+      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_Description);
+      if (c != NULL)
+      {
+        v["Description"] = c;
+      }
+
+      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_OrthancExplorer);
+      v["ExtendsOrthancExplorer"] = (c != NULL);
+
+      call.GetOutput().AnswerJson(v);
+    }
+  }
+
+
+  static void GetOrthancExplorerPlugins(RestApiGetCall& call)
+  {
+    std::string s = "// Extensions to Orthanc Explorer by the registered plugins\n\n";
+
+    if (OrthancRestApi::GetContext(call).HasPlugins())
+    {
+      const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPluginsManager();
+      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetOrthancPlugins();
+
+      std::list<std::string> lst;
+      OrthancRestApi::GetContext(call).GetPluginsManager().ListPlugins(lst);
+
+      for (std::list<std::string>::const_iterator
+             it = lst.begin(); it != lst.end(); it++)
+      {
+        const char* tmp = plugins.GetProperty(it->c_str(), _OrthancPluginProperty_OrthancExplorer);
+        if (tmp != NULL)
+        {
+          s += "/**\n * From plugin: " + *it + " (version " + manager.GetPluginVersion(*it) + ")\n **/\n\n";
+          s += std::string(tmp) + "\n\n";
+        }
+      }
+    }
+
+    call.GetOutput().AnswerBuffer(s, "application/javascript");
+  }
+
+
   void OrthancRestApi::RegisterSystem()
   {
     Register("/", ServeRoot);
@@ -123,5 +212,9 @@
     Register("/tools/execute-script", ExecuteScript);
     Register("/tools/now", GetNowIsoString);
     Register("/tools/dicom-conformance", GetDicomConformanceStatement);
+
+    Register("/plugins", ListPlugins);
+    Register("/plugins/{id}", GetPlugin);
+    Register("/plugins/explorer.js", GetOrthancExplorerPlugins);
   }
 }
--- a/OrthancServer/ServerContext.cpp	Fri Nov 28 12:39:22 2014 +0100
+++ b/OrthancServer/ServerContext.cpp	Thu Dec 04 17:04:40 2014 +0100
@@ -77,7 +77,8 @@
     provider_(*this),
     dicomCache_(provider_, DICOM_CACHE_SIZE),
     scheduler_(Configuration::GetGlobalIntegerParameter("LimitJobs", 10)),
-    plugins_(NULL)
+    plugins_(NULL),
+    pluginsManager_(NULL)
   {
     scu_.SetLocalApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"));
     //scu_.SetMillisecondsBeforeClose(1);  // The connection is always released
@@ -536,4 +537,36 @@
       }
     }
   }
+
+
+  bool ServerContext::HasPlugins() const
+  {
+    return (pluginsManager_ && plugins_);
+  }
+
+
+  const PluginsManager& ServerContext::GetPluginsManager() const
+  {
+    if (HasPlugins())
+    {
+      return *pluginsManager_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  const OrthancPlugins& ServerContext::GetOrthancPlugins() const
+  {
+    if (HasPlugins())
+    {
+      return *plugins_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
 }
--- a/OrthancServer/ServerContext.h	Fri Nov 28 12:39:22 2014 +0100
+++ b/OrthancServer/ServerContext.h	Thu Dec 04 17:04:40 2014 +0100
@@ -49,6 +49,7 @@
 namespace Orthanc
 {
   class OrthancPlugins;
+  class PluginsManager;
 
   /**
    * This class is responsible for maintaining the storage area on the
@@ -91,6 +92,7 @@
     boost::mutex luaMutex_;
     LuaContext lua_;
     OrthancPlugins* plugins_;  // TODO Turn it into a listener pattern (idem for Lua callbacks)
+    const PluginsManager* pluginsManager_;
 
   public:
     class DicomCacheLocker : public boost::noncopyable
@@ -195,13 +197,16 @@
       return scheduler_;
     }
 
-    void SetOrthancPlugins(OrthancPlugins& plugins)
+    void SetOrthancPlugins(const PluginsManager& manager,
+                           OrthancPlugins& plugins)
     {
+      pluginsManager_ = &manager;
       plugins_ = &plugins;
     }
 
     void ResetOrthancPlugins()
     {
+      pluginsManager_ = NULL;
       plugins_ = NULL;
     }
 
@@ -210,5 +215,11 @@
                         ResourceType expectedType);
 
     void SignalChange(const ServerIndexChange& change);
+
+    bool HasPlugins() const;
+
+    const PluginsManager& GetPluginsManager() const;
+
+    const OrthancPlugins& GetOrthancPlugins() const;
   };
 }
--- a/OrthancServer/main.cpp	Fri Nov 28 12:39:22 2014 +0100
+++ b/OrthancServer/main.cpp	Thu Dec 04 17:04:40 2014 +0100
@@ -519,7 +519,7 @@
     pluginsManager.RegisterServiceProvider(orthancPlugins);
     LoadPlugins(pluginsManager);
     httpServer.RegisterHandler(orthancPlugins);
-    context.SetOrthancPlugins(orthancPlugins);
+    context.SetOrthancPlugins(pluginsManager, orthancPlugins);
 #endif
 
     httpServer.RegisterHandler(staticResources);
--- a/Plugins/Engine/OrthancPlugins.cpp	Fri Nov 28 12:39:22 2014 +0100
+++ b/Plugins/Engine/OrthancPlugins.cpp	Thu Dec 04 17:04:40 2014 +0100
@@ -172,10 +172,13 @@
 
   struct OrthancPlugins::PImpl
   {
+    typedef std::pair<std::string, _OrthancPluginProperty>  Property;
+
     typedef std::pair<boost::regex*, OrthancPluginRestCallback> RestCallback;
     typedef std::list<RestCallback>  RestCallbacks;
     typedef std::list<OrthancPluginOnStoredInstanceCallback>  OnStoredCallbacks;
     typedef std::list<OrthancPluginOnChangeCallback>  OnChangeCallbacks;
+    typedef std::map<Property, std::string>  Properties;
 
     ServerContext& context_;
     RestCallbacks restCallbacks_;
@@ -188,6 +191,7 @@
     SharedMessageQueue  pendingChanges_;
     boost::thread  changeThread_;
     bool done_;
+    Properties properties_;
 
     PImpl(ServerContext& context) : 
       context_(context), 
@@ -1022,6 +1026,14 @@
         return true;
       }
 
+      case _OrthancPluginService_SetProperty:
+      {
+        const _OrthancPluginSetProperty& p = 
+          *reinterpret_cast<const _OrthancPluginSetProperty*>(parameters);
+        pimpl_->properties_[std::make_pair(p.plugin, p.property)] = p.value;
+        return true;
+      }
+
       default:
         return false;
     }
@@ -1137,4 +1149,22 @@
 
     return new PluginStorageArea(pimpl_->storageArea_);;
   }
+
+
+
+  const char* OrthancPlugins::GetProperty(const char* plugin,
+                                          _OrthancPluginProperty property) const
+  {
+    PImpl::Property p = std::make_pair(plugin, property);
+    PImpl::Properties::const_iterator it = pimpl_->properties_.find(p);
+
+    if (it == pimpl_->properties_.end())
+    {
+      return NULL;
+    }
+    else
+    {
+      return it->second.c_str();
+    }
+  }
 }
--- a/Plugins/Engine/OrthancPlugins.h	Fri Nov 28 12:39:22 2014 +0100
+++ b/Plugins/Engine/OrthancPlugins.h	Thu Dec 04 17:04:40 2014 +0100
@@ -109,5 +109,8 @@
     IStorageArea* GetStorageArea();
 
     void Stop();
+
+    const char* GetProperty(const char* plugin,
+                            _OrthancPluginProperty property) const;
   };
 }
--- a/Plugins/Engine/PluginsManager.cpp	Fri Nov 28 12:39:22 2014 +0100
+++ b/Plugins/Engine/PluginsManager.cpp	Thu Dec 04 17:04:40 2014 +0100
@@ -205,10 +205,10 @@
     {
       if (it->second != NULL)
       {
-        LOG(WARNING) << "Unregistering plugin '" << CallGetName(*it->second)
-                     << "' (version " << CallGetVersion(*it->second) << ")";
+        LOG(WARNING) << "Unregistering plugin '" << it->first
+                     << "' (version " << it->second->GetVersion() << ")";
 
-        CallFinalize(*(it->second));
+        CallFinalize(it->second->GetLibrary());
         delete it->second;
       }
     }
@@ -226,26 +226,27 @@
   
   void PluginsManager::RegisterPlugin(const std::string& path)
   {
-    std::auto_ptr<SharedLibrary> plugin(new SharedLibrary(path));
+    std::auto_ptr<Plugin> plugin(new Plugin(path));
 
-    if (!IsOrthancPlugin(*plugin))
+    if (!IsOrthancPlugin(plugin->GetLibrary()))
     {
-      LOG(ERROR) << "Plugin " << plugin->GetPath()
+      LOG(ERROR) << "Plugin " << plugin->GetLibrary().GetPath()
                  << " does not declare the proper entry functions";
       throw OrthancException(ErrorCode_SharedLibrary);
     }
 
-    std::string name(CallGetName(*plugin));
+    std::string name(CallGetName(plugin->GetLibrary()));
     if (plugins_.find(name) != plugins_.end())
     {
       LOG(ERROR) << "Plugin '" << name << "' already registered";
       throw OrthancException(ErrorCode_SharedLibrary);
     }
 
+    plugin->SetVersion(CallGetVersion(plugin->GetLibrary()));
     LOG(WARNING) << "Registering plugin '" << name
-                 << "' (version " << CallGetVersion(*plugin) << ")";
+                 << "' (version " << plugin->GetVersion() << ")";
 
-    CallInitialize(*plugin, context_);
+    CallInitialize(plugin->GetLibrary(), context_);
 
     plugins_[name] = plugin.release();
   }
@@ -298,4 +299,37 @@
       }
     }
   }
+
+
+  void PluginsManager::ListPlugins(std::list<std::string>& result) const
+  {
+    result.clear();
+
+    for (Plugins::const_iterator it = plugins_.begin(); 
+         it != plugins_.end(); it++)
+    {
+      result.push_back(it->first);
+    }
+  }
+
+
+  bool PluginsManager::HasPlugin(const std::string& name) const
+  {
+    return plugins_.find(name) != plugins_.end();
+  }
+
+
+  const std::string& PluginsManager::GetPluginVersion(const std::string& name) const
+  {
+    Plugins::const_iterator it = plugins_.find(name);
+    if (it == plugins_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return it->second->GetVersion();
+    }
+  }
+
 }
--- a/Plugins/Engine/PluginsManager.h	Fri Nov 28 12:39:22 2014 +0100
+++ b/Plugins/Engine/PluginsManager.h	Thu Dec 04 17:04:40 2014 +0100
@@ -43,7 +43,34 @@
   class PluginsManager : boost::noncopyable
   {
   private:
-    typedef std::map<std::string, SharedLibrary*>  Plugins;
+    class Plugin
+    {
+    private:
+      SharedLibrary library_;
+      std::string  version_;
+
+    public:
+      Plugin(const std::string& path) : library_(path)
+      {
+      }
+
+      SharedLibrary& GetLibrary()
+      {
+        return library_;
+      }
+
+      void SetVersion(const std::string& version)
+      {
+        version_ = version;
+      }
+
+      const std::string& GetVersion() const
+      {
+        return version_;
+      }
+    };
+
+    typedef std::map<std::string, Plugin*>  Plugins;
 
     OrthancPluginContext  context_;
     Plugins  plugins_;
@@ -67,5 +94,11 @@
     {
       serviceProviders_.push_back(&provider);
     }
+
+    void ListPlugins(std::list<std::string>& result) const;
+
+    bool HasPlugin(const std::string& name) const;
+
+    const std::string& GetPluginVersion(const std::string& name) const;
   };
 }
--- a/Plugins/OrthancCPlugin/OrthancCPlugin.h	Fri Nov 28 12:39:22 2014 +0100
+++ b/Plugins/OrthancCPlugin/OrthancCPlugin.h	Thu Dec 04 17:04:40 2014 +0100
@@ -153,6 +153,13 @@
 #endif
 
   /**
+   * Forward declaration of one of the mandatory functions for Orthanc
+   * plugins.
+   **/
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName();
+
+
+  /**
    * The various HTTP methods for a REST call.
    **/
   typedef enum
@@ -241,6 +248,7 @@
     _OrthancPluginService_GetOrthancPath = 4,
     _OrthancPluginService_GetOrthancDirectory = 5,
     _OrthancPluginService_GetConfigurationPath = 6,
+    _OrthancPluginService_SetProperty = 7,
 
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -281,6 +289,14 @@
   } _OrthancPluginService;
 
 
+  typedef enum
+  {
+    _OrthancPluginProperty_Description = 1,
+    _OrthancPluginProperty_RootUri = 2,
+    _OrthancPluginProperty_OrthancExplorer = 3
+  } _OrthancPluginProperty;
+
+
 
   /**
    * The memory layout of the pixels of an image.
@@ -1712,6 +1728,62 @@
 
 
 
+  typedef struct
+  {
+    const char* plugin;
+    _OrthancPluginProperty property;
+    const char* value;
+  } _OrthancPluginSetProperty;
+
+
+  /**
+   * @brief Set the URI where the plugin provides its Web interface.
+   *
+   * For plugins that come with a Web interface, this function
+   * declares the entry path where to find this interface. This
+   * information is notably used in the "Plugins" page of Orthanc
+   * Explorer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri The root URI for this plugin.
+   **/ 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri(
+    OrthancPluginContext*  context,
+    const char*            uri)
+  {
+    _OrthancPluginSetProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_RootUri;
+    params.value = uri;
+
+    context->InvokeService(context, _OrthancPluginService_SetProperty, &params);
+  }
+
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription(
+    OrthancPluginContext*  context,
+    const char*            description)
+  {
+    _OrthancPluginSetProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_Description;
+    params.value = description;
+
+    context->InvokeService(context, _OrthancPluginService_SetProperty, &params);
+  }
+
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer(
+    OrthancPluginContext*  context,
+    const char*            javascript)
+  {
+    _OrthancPluginSetProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_OrthancExplorer;
+    params.value = javascript;
+
+    context->InvokeService(context, _OrthancPluginService_SetProperty, &params);
+  }
 
 #ifdef  __cplusplus
 }
--- a/Plugins/Samples/Basic/Plugin.c	Fri Nov 28 12:39:22 2014 +0100
+++ b/Plugins/Samples/Basic/Plugin.c	Thu Dec 04 17:04:40 2014 +0100
@@ -326,6 +326,11 @@
 
   OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
 
+  /* Declare several properties of the plugin */
+  OrthancPluginSetRootUri(context, "/plugin/hello");
+  OrthancPluginSetDescription(context, "This is the description of the sample plugin that can be seen in Orthanc Explorer.");
+  OrthancPluginExtendOrthancExplorer(context, "alert('Hello Orthanc! From sample plugin with love.');");
+
   /* Make REST requests to the built-in Orthanc API */
   OrthancPluginRestApiGet(context, &tmp, "/changes");
   OrthancPluginFreeMemoryBuffer(context, &tmp);
--- a/Resources/OrthancPlugin.doxygen	Fri Nov 28 12:39:22 2014 +0100
+++ b/Resources/OrthancPlugin.doxygen	Thu Dec 04 17:04:40 2014 +0100
@@ -709,7 +709,7 @@
 # wildcard * is used, a substring. Examples: ANamespace, AClass,
 # AClass::ANamespace, ANamespace::*Test
 
-EXCLUDE_SYMBOLS        = _OrthancPlugin*
+EXCLUDE_SYMBOLS        = _OrthancPlugin* OrthancPluginGetName
 
 # The EXAMPLE_PATH tag can be used to specify one or more files or
 # directories that contain example code fragments that are included (see