CTP Plugins
This article describes how to add functionality to CTP through the CTP Plugin mechanism. The intended audience for this article is software engineers who are extending or maintaining the code.
CTP has three basic components:
- The embedded servlet container provides an HTTP server with support for server-side computation through a simplified, non-W3C-compliant servlet mechanism implemented in the org.rsna.server and org.rsna.servlets packages. See The Util Module for more information.
- The Pipeline mechanism supports ordered sequences of processing stages that implement the org.rsna.ctp.PipelineStage interface. See Pipelines for more information.
- The Plugin mechanism supports adding functionality into the program outside the framework of pipelines and pipeline stages. Plugins can be used to add servlets into a system, modify the configuration, and provide services to pipeline stages.
This article will concentrate on the design of plugins. It assumes familiarity with the other articles listed in the Articles for Developers and Planners section of CTP Articles.
1 Building and Deploying a Plugin
To implement a plugin, first set up a development environment as described in Setting Up a MIRC Development Environment and build the Util and CTP modules. This provides the jars that must be referenced in the build of the plugin, and equally important, it provides all the Javadocs.
As described in Building an Extension JAR, the best way to deploy CTP extensions is to put them in jars that are placed in the CTP/libraries directory or any of its subdirectories.
2 The Plugin Interface
To be recognized as a Plugin, a class must implement the org.rsna.ctp.plugin.Plugin interface. An abstract class, org.rsna.ctp.plugin.AbstractPlugin, is provided to supply some of the basic methods required by the Plugin interface. All the standard plugins extend this class.
The Javadocs explain the methods which must be implemented in a Plugin.
Each Plugin class must have a constructor that takes its configuration file XML Element as its argument. The constructor must obtain any configuration information it requires from the element. While it is not required that all configuration information be placed in attributes of the element, the getConfigHTML method provided by AbstractPlugin expects it, and if you choose to encode configuration information in another way, you must override the getConfigHTML method to make that information available to the configuration servlet.
3 The Plugin Lifecycle
Like all CTP components, a plugin is instantiated when the system starts. At the time the class is instantiated, the rest of the system configuration is not yet available, so the constructor must perform only those tasks that can be accomplished with the information contained in its configuration element. That is, it cannot access other plugins or pipeline stages.
Once CTP has instantiated all the configured components, it calls the start method of every component. CTP starts all the plugins first and only then starts the pipeline stages. Thus, a pipeline stage that references a plugin can assume that the plugin is available when its start method is called.
Plugins that are configured with id attributes are indexed by the org.rsna.ctp.Configuration class and can be found by other plugins or pipeline stages through the getRegisteredPlugin method like this:
- Plugin thePlugin = Configuration.getInstance().getRegisteredPlugin(thePluginID);
When CTP shuts down, it calls the shutdown method of every component. CTP shuts down all the pipeline stages first and only then shuts down the plugins. Thus, a pipeline stage that references a plugin can use the plugin if necessary while its shutdown method is running.
The org.rsna.ctp.plugin.Plugin interface provides an isDown method to allow CTP to know when the plugin is down. Plugins that take some time to shut down typically return from the shutdown method as soon as they have initiated the shutdown, but they don't return true from the isDown method until the the shutdown is complete. The org.rsna.ctp.plugin.AbstractPlugin class handles this handshaking, and plugins that extend that class only have to override the shutdown method if they have special things to do (commit a database, close files, etc.).
4 Interfacing to the Configuration Editor
The CTP Launcher.jar program provides a manual way of starting, stopping, and monitoring CTP. It also includes a very convenient configuration editor. See The CTP Launcher Configuration Editor for a description of the user interface. The configuration editor is driven by XML files. When the Launcher program starts, the configuration editor searches the CTP/libraries directory and all its subdirectories for jar files containing a file named ConfigurationTemplates.xml located in the root of the jar file's directory tree. It combines the contents of all such files into a single XML DOM object and uses that object to drive the editor's user interface. A simple example of a ConfigurationTemplates.xml file that might be in a jar for a plugin is shown here:
<TemplateDefinitions> <Components> <Plugin> <attr name="name" required="yes" default="Redirector"/> <attr name="class" required="yes" default="org.rsna.ctp.stdplugins.Redirector" editable="no"/> <attr name="httpPort" required="yes" default="80"> <helptext>The HTTP port on which to listen.</helptext> </attr> <attr name="httpsHost" required="yes" default="mirc.mysecuresite.myuniversity.edu"> <helptext>The IP address or domain name to which to redirect requests.</helptext> </attr> <attr name="httpsPort" required="yes" default="443"> <helptext>The HTTPS port to which to redirect requests.</helptext> </attr> </Plugin> </Components> </TemplateDefinitions>
The schema for the ConfigurationTemplates.xml file has somewhat more capability, although that capability is not necessary for most CTP extensions. A complete example can be found in the source code for CTP in the CTP/source/resources/ConfigurationTemplates.xml file.
5 Examples
5.1 The Redirector Plugin
The CTP servlet container can operate on HTTP or HTTPS, but not both simultaneously. On sites that use HTTPS, it is sometimes convenient to provide a redirect service on port 80 to switch the user to the HTTPS port. CTP includes a standard plugin to provide this function. For convenience, all the code is shown below. Note the division of work between the constructor and the start method. Note also that because the shutdown for this plugin is fast, no attempt is made to return before it is down. Finally, it is generally a good idea to log the status of startup and shutdown. This can be a big help in debugging problems in the field.
/*--------------------------------------------------------------- * Copyright 2013 by the Radiological Society of North America * * This source software is released under the terms of the * RSNA Public License (http://mirc.rsna.org/rsnapubliclicense) *----------------------------------------------------------------*/ package org.rsna.ctp.stdplugins; import org.apache.log4j.Logger; import org.rsna.ctp.plugin.AbstractPlugin; import org.rsna.server.HttpRequest; import org.rsna.server.HttpResponse; import org.rsna.service.HttpService; import org.rsna.service.Service; import org.rsna.util.StringUtil; import org.w3c.dom.Element; /** * A Plugin to monitor an HTTP port and redirect connections to an HTTPS port. */ public class Redirector extends AbstractPlugin { static final Logger logger = Logger.getLogger(Redirector.class); int httpPort; String httpsHost; int httpsPort; HttpService monitor = null; Service handler = null; /** * Construct a plugin implementing a Redirector. * @param element the XML element from the configuration file * specifying the configuration of the plugin. */ public Redirector(Element element) { super(element); httpPort = StringUtil.getInt(element.getAttribute("httpPort"), 80); httpsHost = element.getAttribute("httpsHost").trim(); httpsPort = StringUtil.getInt(element.getAttribute("httpsPort"), 443); try { handler = new RedirectionHandler(); monitor = new HttpService(false, httpPort, handler); logger.info("Redirector Plugin instantiated"); } catch (Exception ex) { logger.warn("Unable to instantiate the Redirector plugin on port "+httpPort); } } /** * Start the plugin. */ public void start() { if (monitor != null) { monitor.start(); logger.info("Redirector Plugin started on port "+httpPort+"; target port: "+httpsPort); } } /** * Stop the plugin. */ public void shutdown() { if (monitor != null) { monitor.stopServer(); logger.info("Redirector Plugin stopped"); } stop = true; } class RedirectionHandler implements Service { public RedirectionHandler() { } public void process(HttpRequest req, HttpResponse res) { String host = httpsHost; if (host.equals("")) { req.getHost(); int k = host.indexOf(":"); if (k >= 0) host = host.substring(0,k); } host += ":" + httpsPort; String query = req.getQueryString(); if (!query.equals("")) query = "?" + query; String url = "https://" + host + req.getPath() + query; res.redirect(url); } } }
5.2 The MIRC Plugin
The entire MIRC Teaching File System is implemented as a single plugin. The plugin installs servlets, opens databases, ensures that the user accounts are at least minimally correct, and starts background threads for various purposes. Note that most of this work must be done in the start method because it requires access to the org.rsna.ctp.Configuration object and the servlet container. For convenience, the code is shown here:
/** * The class that represents a single MIRC site plugin. */ public class MIRC extends AbstractPlugin { static final Logger logger = Logger.getLogger(MIRC.class); File configFile = null; /** * Construct a plugin implementing a MIRC site. * @param element the XML element from the configuration file * specifying the configuration of the plugin. */ public MIRC(Element element) { super(element); //Install the config file if necessary. configFile = new File(root, "mirc.xml"); FileUtil.getFile(configFile, "/mirc/mirc.xml"); logger.info("MIRC Plugin instantiated"); } /** * Start the plugin. */ public void start() { //Load the MIRC configuration. //Note: this must be done here, rather than in the constructor //because the CTP Configuration is not yet instantiated when //the constructor is called, and MircConfig calls the CTP //configuration to obtain the server IP address and port. MircConfig mc = MircConfig.load(configFile); //Load the Preferences Preferences prefs = Preferences.load( root ); //Load the DownloadDB DownloadDB.load( root ); //Load the ScoredQuizDB ScoredQuizDB.load( root ); //Load the ActivityDB ActivityDB.load( root ); //Install the servlets Configuration config = Configuration.getInstance(); ServletSelector selector = config.getServer().getServletSelector(); selector.addServlet("users", MircUserManagerServlet.class); selector.addServlet("mirc", MircServlet.class); selector.addServlet("query", QueryService.class); selector.addServlet("qsadmin", QueryServiceAdmin.class); selector.addServlet("casenav", CaseNavigatorService.class); selector.addServlet("confs", ConferenceService.class); selector.addServlet("delete", DeleteService.class); selector.addServlet("files", FileService.class); selector.addServlet("fsadmin", FileServiceAdmin.class); selector.addServlet("challenge", ChallengeServlet.class); selector.addServlet("radlex", RadLexSuggest.class); selector.addServlet("reset", ResetService.class); selector.addServlet("revert", RevertService.class); selector.addServlet("sort", SortImagesService.class); selector.addServlet("storage", StorageService.class); selector.addServlet("ssadmin", StorageServiceAdmin.class); selector.addServlet("submit", SubmitService.class); selector.addServlet("summary", AuthorSummary.class); selector.addServlet("activity", ActivityReport.class); selector.addServlet("prefs", PreferencesServlet.class); selector.addServlet("zip", ZipService.class); selector.addServlet("bauth", BasicAuthorService.class); selector.addServlet("aauth", AuthorService.class); selector.addServlet("addimg", AddImageService.class); selector.addServlet("publish", PublishService.class); selector.addServlet("download", DownloadServlet.class); selector.addServlet("comment", CommentService.class); selector.addServlet("myrsna", MyRSNAServlet.class); selector.addServlet("quiz", QuizServlet.class); selector.addServlet("quizmgr", QuizManagerServlet.class); selector.addServlet("quizsummary", QuizSummaryServlet.class); selector.addServlet("quizanswers", QuizAnswerSummaryServlet.class); selector.addServlet("presentation", PresentationService.class); //Install the standard roles Users users = Users.getInstance(); users.addRole("publisher"); users.addRole("author"); users.addRole("update"); users.addRole("department"); //Make sure the admin has the MIRC roles User admin = users.getUser("admin"); if (admin != null) { admin.addRole("author"); admin.addRole("publisher"); admin.addRole("department"); //Set a person name for the admin user //if it doesn't already have one. Element pref = prefs.get("admin", true); if (pref == null) { prefs.setAuthorInfo("admin", "Administrator", "", ""); } else { String name = pref.getAttribute("name"); if (name.equals("")) { prefs.setAuthorInfo("admin", "Administrator", pref.getAttribute("affiliation"), pref.getAttribute("contact")); } } } logger.info("MIRC Plugin started"); //Install the defined roles mc.setDefinedRoles(); //Install the redirector installRedirector(); //Load the RadLex index RadLexIndex.loadIndex(root); //Start the LibraryMonitor new LibraryMonitor().start(); //Start the DraftDocumentMonitors Set<String> ssids = mc.getLocalLibraryIDs(); for (String ssid : ssids) { Element lib = mc.getLocalLibrary(ssid); if (lib != null) { int timeout = StringUtil.getInt(lib.getAttribute("timeout")); if (timeout > 0) new DraftDocumentMonitor(ssid, timeout).start(); } } //Start the Activity Summary Report Submitter new SummarySubmitter().start(); } /** * Stop the plugin. */ public void shutdown() { Index.closeAll(); RadLexIndex.close(); Preferences.close(); DownloadDB.close(); ActivityDB.close(); stop = true; logger.info("MIRC Plugin stopped"); } //Copy the redirector into the root of the server private void installRedirector() { FileOutputStream out = null; InputStream in = null; try { File serverRoot = Configuration.getInstance().getServer().getServletSelector().getRoot(); File indexHTML = new File(serverRoot, "index.html"); out = new FileOutputStream(indexHTML); String redirector = "/mirc/redirector.html"; in = getClass().getResourceAsStream(redirector); FileUtil.copy(in, out, -1); } catch (Exception ignore) { } FileUtil.close(in); FileUtil.close(out); } /** * Get HTML text displaying the current status of the plugin. * @return HTML text displaying the current status of the plugin. */ public String getStatusHTML() { return getStatusHTML(""); } }
5.3 The AuditLog Plugin
CTP provides an audit logging plugin that allows pipeline stages or other plugins to log text in a database that is accessible to an admin user. This feature was originally intended for use in 21CFR11-compliant clinical trials, but it has been used for many other purposes. Several standard pipeline stages (HttpExportService, DicomExportService, and FtpExportService) can be configured to record audit log entries when exporting data objects. The main body of the code is shown below. Note the installation of the servlet in the start method.
/** * A Plugin to implement an audit log repository that can be * accessed through a servlet. */ public class AuditLog extends AbstractPlugin { static final Logger logger = Logger.getLogger(AuditLog.class); static final String databaseName = "AuditLog"; static final String defaultID = "auditlog"; static final String lastIDName = "__lastID"; String servletContext; private RecordManager recman; private HTree count = null; private HTree entryTable = null; private HTree contentTypeTable = null; private HTree timeTable = null; private HTree patientIDIndex = null; private HTree studyUIDIndex = null; private HTree objectUIDIndex = null; /** * Construct a plugin implementing an audit log repository . * @param element the XML element from the configuration file * specifying the configuration of the plugin. */ public AuditLog(Element element) { super(element); //See if there is a valid ID. //The ID is used as the context of the servlet. id = id.replaceAll("\\s+", ""); if (id.equals("")) id = defaultID; //Open the database try { File dbFile = new File(root, databaseName); recman = JdbmUtil.getRecordManager(dbFile.getAbsolutePath()); count = JdbmUtil.getHTree(recman, "count"); entryTable = JdbmUtil.getHTree(recman, "entry"); contentTypeTable = JdbmUtil.getHTree(recman, "contentType"); timeTable = JdbmUtil.getHTree(recman, "time"); patientIDIndex = JdbmUtil.getHTree(recman, "patientID"); studyUIDIndex = JdbmUtil.getHTree(recman, "studyUID"); objectUIDIndex = JdbmUtil.getHTree(recman, "objectUID"); } catch (Exception unable) { logger.warn("Unable to open the AuditLog database."); } logger.info("AuditLog Plugin instantiated"); } /** * Start the plugin. */ public void start() { //Install the servlet Configuration config = Configuration.getInstance(); ServletSelector selector = config.getServer().getServletSelector(); selector.addServlet(id, AuditLogServlet.class); logger.info("AuditLog Plugin started with context \""+id+"\""); } /** * Stop the plugin. */ public void shutdown() { if (recman != null) { try { recman.commit(); recman.close(); recman = null; } catch (Exception ignore) { } } stop = true; logger.info("AuditLog Plugin stopped"); } ...etc.