View on GitHub

Resources for Dicoogle users and developers.

Developing Plugins

Dicoogle is extendable in deployment time, thanks to its plugin-based architecture. In order to integrate additional features over Dicoogle, you may create your own plugin set. A PluginSet is a composition of plugins developed with the intent of supporting a given functionality. There are 5 particular types of plugins:

  • Storage plugins are responsible for storing and retrieving data. A basic implementation would keep files in the local file system, but Dicoogle can be extended to support remote storage with plugins of this type.
  • Indexer plugins implement index generation. A fully deployed instance of Dicoogle should have at least one DICOM indexer.
  • Query plugins provide a means of querying the indexed data. Often a query provider is coupled with a particular indexer, and are bundled together in the plugin set.
  • Jetty Service plugins support the attachment of Eclipse jetty servlets, so as to host new web services in Dicoogle.
  • Rest Web Service plugins contain a Restlet server resource that can be attached to Dicoogle, also for hosting web services.
  • Web User Interface Plugins, unlike other kinds of plugins, are developed in JavaScript and provide new UI components that are automatically loaded into Dicoogle’s web application.

This section assumes familiarity with the Java programming language.

Basic concepts

All of the necessary data structures and interfaces for plugin development are in the Dicoogle SDK project. Before getting straight to coding, it is important to understand the tools and mechanisms that you will be using.

Registering Plugins

Marking certain classes as plugins is done with a plugin set. Create a class that implements PluginSet and apply the @PluginImplementation annotation on the class, which allows the plugin framework to fetch the set from the core platform. The constructors should create one instance of each plugin intended, and the plugin getters should provide an immutable list of plugins. When a plugin set does not provide any plugins of a certain type, the respective getter should return an empty list (such as Collections.EMPTY_LIST). Moreover, name getters should provide a simple, unique name for all plugins of that type. For instance, a query provider and indexer can share the same name, but two distinct query providers can not.

Here’s a minimal example:

@PluginImplementation
public class MyPluginSet implements PluginSet {
    // use slf4j for logging purposes
    private static final Logger logger = LoggerFactory.getLogger(MyPluginSet.class);
    
    // You can list each of our plugins as an attribute to the plugin set
    private final MyQueryProvider query;
    
    // Additional resources may be added here.
    private ConfigurationHolder settings;
    
    public MyPluginSet() throws IOException {
        logger.info("Initializing My Plugin Set");

        // construct all plugins here
        this.query = new MyQueryProvider();
        
        logger.info("My Plugin Set is ready");
    }

    @Override
    public Collection<QueryInterface> getQueryPlugins() {
        return Collections.singleton((QueryInterface) this.query);
    }
    
    @Override
    public String getName() {
        return "mine";
    }

    // ... implement the remaining methods
}

Dicoogle Platform API

Interactions with the core platform are made via the PlatformInterface. This is the top-level API of Dicoogle that is exposed to other plugins.

In order to obtain this platform interface, plugins (or the plugin set) need to implement the interface PlatformCommunicatorInterface. The method setPlatformProxy declared therein behaves like a callback, which will be called by the platform shortly after the plugin is loaded. Usually, plugins can simply pass the argument into an attribute for future use:

public class MyQueryPlugin 
        implements QueryInterface, PlatformCommunicatorInterface {
    private DicooglePlatformInterface platform;

    // ... other content

    @Override
    void setPlatformProxy(DicooglePlatformInterface platform) {
        this.platform = platform;
    }
}

This object contains the set of methods that you can use. Here is the full list of methods in version 2.4.0:

IndexerInterface requestIndexPlugin(String name);
QueryInterface requestQueryPlugin(String name);
Collection<IndexerInterface> getAllIndexPlugins();
Collection<QueryInterface> getAllQueryPlugins();
StorageInterface getStoragePluginForSchema(String scheme);
StorageInterface getStorageForSchema(URI location);
Iterable<StorageInputStream> resolveURI(URI location, Object ...args);
Collection<StorageInterface> getStoragePlugins(boolean onlyEnabled);
StorageInterface getStorageForSchema(String scheme);
Collection<QueryInterface> getQueryPlugins(boolean onlyEnabled);
List<String> getQueryProvidersName(boolean enabled);
QueryInterface getQueryProviderByName(String name,
        boolean onlyEnabled);
JointQueryTask queryAll(JointQueryTask holder, String query,
        Object... parameters) ;
Task<Iterable<SearchResult>> query(String querySource, String query,
        Object... parameters);
JointQueryTask query(JointQueryTask holder,
        List<String> querySources, String query, Object... parameters);
List<Task<Report>> index(URI path);
List<Report> indexBlocking(URI path);
ServerSettingsReader getSettings();

Frequently Asked Questions

Here is a list of questions frequently made when developing plugins for Dicoogle.

How do I query for DICOM meta-data?

There should be at least one DIM content provider in a deployed instance of Dicoogle. Let us assume that the plugin is named “lucene”. First retrieve the appropriate QueryInterface, then call the query method with the intended query. For DICOM meta-data providers, the query should follow the Apache Lucene query language.

QueryInterface provider = this.platform.getQueryPlugin("lucene");
Iterable<SearchResult> results = provider.query("Modality:CT AND AXIAL");
for (SearchResult res: results) {
   // use results
}

The outcome is a sequence of search results, which is possibly lazy. You should not traverse the outcome more than once. In order to manipulate the list further, please save the results into a list such as ArrayList.

At the moment, plugins that rely on DICOM content are recommended to support a configurable DIM query source, rather than hard-coding “lucene” as the provider. Future versions of Dicoogle should provide a means to retrieve the default DIM query source directly from the core platform, as this is a planned feature.

How do I access files in storage?

Dicoogle provides an abstraction for accessing files from any kind of data source. Instead of using standard Java I/O APIs, plugins should retrieve the appropriate storage interface (StorageInterface class). Once with the intended storage, the method at can be used to obtain a sequence of all files at the given location.

URI uri = ...;
StorageInterface store = this.platform.getStorageForSchema(uri);
Iterable<StorageInputStream> files = store.at(uri);

StorageInputStream is an abstraction for files in a storage (like a blob of data, not necessarily in the file system), from which a raw input stream can be retrieved.

What if I want to retrieve a file by SOPInstanceUID?

First query the DIM provider for the file with that UID, then retrieve the URI from the search result:

String uid = "1.2.3.4";
QueryInterface dimProvider = this.platform.getQueryProviderByName("lucene", true);
Iterator<SearchResult>> results = dimProvider.("SOPInstanceUID:" + uid).iterator();
if (results.hasNext()) {
    SearchResult res = results.next();
    URI uri = res.getURI();
    Iterable<StorageInputStream> files = this.platform.getStorageForSchema(uri).at(uri);
    // use files
} else {
    // no such file
}

How do I read and write settings?

All plugins and plugin sets implement the method setSettings, which is also similar to a callback. The platform will call this method with a configuration holder after instantiation.

A typical implementation of this method should save the configuration holder to an attribute and check that the settings are ok. Fetching the actual settings currently yields an Apache Commons 1.x XmlConfiguration object (a user guide can be read here). The method may also write missing fields with default values.

@Override
public void setSettings(ConfigurationHolder configurationHolder) {
    this.settings = configurationHolder;

    XmlConfiguration configuration = this.settings.getConfiguration();

    try {
        // required field, will throw if missing
        String uid = configuration.getString("service-uid");
    } catch (RuntimeException ex) {
        logger.warn("Failed to configure plugin: required fields are missing!", ex);
    }

    // optional field, default is 1
    int numResources = configuration.getInt("num-resources", 1);
    configuration.setProperty("num-resources", numResources); // write field

    try {
        configuration.save();
    } catch (ConfigurationException ex) {
        logger.warn("Failed to save configurations!", ex);
    }
    this.uid = uid;
    this.numResources = numResources;
}

// And don't forget to implement `getSettings()`!
@Override
public ConfigurationHolder getSettings() {
    return this.settings;
}

Also note that, in the latest version of Dicoogle, the plugin will be disabled if this method throws an unchecked exception.

This regards plugin-specific settings. In order to read and write global Dicoogle settings, the platform API provides the necessary methods.

How do I create new web services?

Web services are one of the most flexible ways of expanding Dicoogle with new features. Currently, there are two ways to achieve this:

  • Jetty Servlets can be created and registered using a plugin of type JettyPluginInterface. Create your own servlets (see HttpServlet), then attach them into a handler list in getJettyHandlers:
@Override
public HandlerList getJettyHandlers() {

    // encapsulate servlets into holders, then add them to handlers.
    ServletContextHandler handler = new ServletContextHandler();
    handler.setContextPath("/sample");
    handler.addServlet(new ServletHolder(this.webService), "/hello");
        
    // you can retrieve plugin-scoped resources
    URL url = RSIJettyPlugin.class.getResource("/WEBAPP");
    String directoryToServeAssets = url.toString();
        
    // web app contexts are more appropriate for serving web pages
    final WebAppContext webpages = new WebAppContext(directoryToServeAssets, "/dashboardSample");
    webpages.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "true"); // disables directory listing
    webpages.setWelcomeFiles(new String[]{"index.html"});

    // add all handlers to a handler list and return it
    HandlerList l = new HandlerList();
    l.addHandler(handler);
    l.addHandler(webpages);

    return l;
}
  • Rest Service plugins consider a subset of the Restlet framework API, allowing developers to create and attach simple server resources. Their integration is simpler, although more brittle. A proposal for better Restlet support is currently in review. In the mean time, server resources can be implemented and integrated by creating a new ServerResource like this:
public class RSIWebResource extends ServerResource {
    
    @Get
    public Representation test() {
        StringRepresentation sr = new StringRepresentation("{\"name\":\"rsi\"}");
        sr.setMediaType(MediaType.APPLICATION_JSON);
        return sr;
    }
    
    // You can handle all CRUD operations. More information in the Restlet documentation.
    
    /** `toString` defines the service endpoint. */
    @Override
    public String toString() {
        return "service/endpoint/test";
    }

}

In either case, do not forget to register all plugins in the plugin set.

Should I use System.out for logging in my plugins?

Printing directly to the standard output is not recommended. Dicoogle uses slf4j for all logging purposes, and so its plugins should rely on this API as well. Please see the slf4j user manual. The FAQ also provides excellent tips on how to use (and how not to use) the API. In particular:

  • Avoid performing concatenations in the logged text (i.e. do not write logger.info("Status: " + status);); use template matching instead (e.g. logger.info("Status: {}", status);).
  • Do not call toString() on the template arguments, as this is done automatically and only when needed.
  • Restrict ERROR level log instructions to situations where something critical occurred in the application, often associated to bugs in the software, and that should be attended by an administrator. Less critical issues should be logged with the WARNING level or lower.
  • Logging lines for debugging purposes should be at either level DEBUG or TRACE. You can configure Dicoogle to show these messages with a custom log4j2 configuration file, such as the one below. The JVM variable log4j.configurationFile should then be defined as thus:
java -Dlog4j.configurationFile=log4j2.xml -jar "dicoogle.jar" -s
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="STDOUT" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p %C{2} (%F:%L) - %m%n"/>
        </Console>
        <RollingRandomAccessFile name="Rolling" fileName="dicoogle.log" filePattern="dicoogle-%i.log" >
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} | %-5p [%t] (%F:%L) - %m%n"/>
            <Policies>
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="2.0 MB"/>
            </Policies>
        </RollingRandomAccessFile>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="STDOUT" level="info" />
            <AppenderRef ref="Rolling" level="info" />
        </Root>
        <Logger name="pt.ua.dicoogle" additivity="false">
            <AppenderRef ref="STDOUT" level="info" />
            <AppenderRef ref="Rolling" level="debug" />
        </Logger>
        <Logger name="org.eclipse.jetty" additivity="false">
            <AppenderRef ref="STDOUT" level="warn" />
            <AppenderRef ref="Rolling" level="info" />
        </Logger>

        <!-- configure logger/appender pair separately to reduce noise -->
        <Logger name="pt.ua.dicoogle.my.plugin" additivity="false">
            <AppenderRef ref="STDOUT" level="trace" />
            <AppenderRef ref="Rolling" level="trace" />
        </Logger>
    </Loggers>
</Configuration>