Mercurial > hg > orthanc
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> » 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> » 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, ¶ms); + } + + + 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, ¶ms); + } + + + 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, ¶ms); + } #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