Page tree
Skip to end of metadata
Go to start of metadata

The starting basis of an XNAT plugin project is the set of build files that provide the build logic, library dependencies, and configuration for building your plugin, as well as integrating special functionality required to process XNAT data-type schemas and process specially annotated classes and properties files. XNAT plugins use the Gradle build tool for these functions. To get started, you need two files:

  • build.gradle is the primary build file for a Gradle build. It includes information like what types of functions should be used in the build (through the use of Gradle plugins), what other libraries your build depends on, version, and so on.
  • settings.gradle defines various attributes about the Gradle build. For most standard XNAT plugin builds, the only thing you'll set in here will be the name of the build project.

Together these are called the Gradle build files or Gradle project configuration. This page describes how to create a new XNAT plugin by configuring these files.

Gradle is a build tool that's based on the Groovy programming language. If you expect to work with XNAT plugins with any regularity, it's a good idea to get familiar with both Gradle and Groovy. There are lots of good books and tutorials available. There are also a number of excellent tools available for working with Groovy and Gradle:

  • If you are on OSX, Linux, or use Cygwin or Powershell on Windows, you can install and manage Groovy and Gradle with SDKMAN, which is an easy-to-use tool that provides a number of different programming languages and development tools.
  • IntelliJ IDEA provides the ability to write and run Groovy and Gradle scripts. The Community Edition is available for free.
  • Eclipse also provides the ability to write and run Groovy and Gradle scripts through plugins like Buildship. Eclipse is a free and open-source product.

You can download the Creating an XNAT Plugin Project and Creating an XNAT Plugin Project files from the sample template plugin project if you'd like to look at the build file that contains the examples on this page.

Getting Started

The easiest way to start a new XNAT plugin project is by copying an existing XNAT plugin project. There are a number of projects available in the XNAT Tools listing that you can start with, but the XNAT Template Plugin project is annotated with comments to help you easily find where you can configure a particular feature of your plugin. You can start with this or another plugin by cloning the source code repository :

$ git clone git@bitbucket.org:xnatx/xnat-template-plugin.git

This command should clone the repository and put it in a folder named xnat-template-plugin under your current folder.

You can download repositories hosted on Bitbucket without using Git or Mercurial. One of the options on the front page of the repository is Downloads:

The top download option is always Download repository. If you click that, you'll download the contents of the repository in a zip file without any of the source-control metadata.

If you have Gradle installed, you can get these files started by typing the following command in the folder where you want to develop your plugin:

Initializing Gradle build
$ gradle init

This results in a few files being generated:

$ ls -l
total 40
-rw-r--r--  1 xnatuser  staff  1170 Jan 23 14:16 build.gradle
drwxr-xr-x  3 xnatuser  staff   102 Jan 23 14:16 gradle/
-rwxr-xr-x  1 xnatuser  staff  5299 Jan 23 14:16 gradlew*
-rw-r--r--  1 xnatuser  staff  2260 Jan 23 14:16 gradlew.bat
-rw-r--r--  1 xnatuser  staff   584 Jan 23 14:16 settings.gradle

Notice the gradle folder as well as the gradlew and gradlew.bat files. One of the cool features of Gradle is that, if you download a project that uses Gradle as its build tool, you can run it without having actually installed Gradle. The gradlew* files are called "Gradle wrapper scripts" and are script files that check whether the version of Gradle specified in a properties file under the gradle folder is installed locally. If so, the scripts will use the already downloaded Gradle tool, but if not, the specified version of Gradle is downloaded and installed. This does not result in a full application installation of Gradle, but just the files required to run the build, and is usually installed in the user's home folder under a hidden folder named .gradle. You can read more about the wrapper feature in the Gradle documentation.

Once you have your build.gradle and settings.gradle files, whether by copying or generating them, you can start modifying them to set up your own plugin project. There are a number of required and optional elements in your Gradle configuration, as described in the next sections.

Identifying Your Plugin

The first thing to do is provide a unique identifier for your plugin project. Most modern dependency resolvers use three attributes to uniquely identify a build "artifact" (which is usually what you think of when you think about the final product of a build):

  • Group ID: This identifies the overall project to which your plugin belongs. This may map directly to an institution or company, but may also identify project groups or development efforts within the institution or company. This is conventionally based on the primary domain name for the group, with one or more optional subgroups. Oh, and it's backwards! So if you're developing for a neuroimaging group within a radiology department at oldschool.edu, your group ID might be edu.oldschool.rad.ni.
  • Artifact ID: This identifies the specific project that you're building. It should be unique within groups developing under the same group ID.
  • Version: This is the version of the specific project. You can have multiple instances of the group ID and artifact ID as long as they differ by version. By convention, release versions are something like A.B.C, where AB, and C are all integers, while development versions use the same structure but attach a qualifier like "-SNAPSHOT" or "-BETA" to the version.

If you don't intend to release your plugin for general usage, these attributes aren't as important. If you do plan to submit your plugins to the XNAT Marketplace or otherwise available, you should take care when choosing the values for these attributes.

To set these values, you'll need to edit both of your Gradle configuration files. The project ID and version are set in build.gradle:

Setting project ID and version in build.gradle
group 'edu.oldschool.rad.ni'
version '1.0.0-SNAPSHOT'

The project name (which is what Gradle uses for the artifact ID) is set in settings.gradle:

Setting project name in settings.gradle
rootProject.name = 'my-xnat-plugin'

Configuring Gradle Plugins

It is really important to understand the distinction between an XNAT plugin and a Gradle plugin:

  • An XNAT plugin is a self-contained file, the result of building your XNAT plugin project, that can be installed in an XNAT server to extend XNAT's functionality
  • A Gradle plugin actually extends Gradle's functionality, providing extra tasks and targets for the Gradle build

In the course of building your XNAT plugin, you can use Gradle plugins to make the build work. At the very least, there is a Gradle plugin called xnat-data-builder, described in more detail later in this section, that helps convert your various data-type schemas and code into an XNAT plugin.

The easiest way to make this distinction is that Gradle plugins help you build your XNAT plugin. Once you have your XNAT plugin, you're done with Gradle plugins until the next time you need to build.

You'll need the following plugins for most XNAT plugin projects:

  • The java plugin provides a number of tasks for building and packaging Java classes
  • The maven plugin provides the functionality to find and download libraries from Maven repositories
  • The xnat-data-builder plugin generates processes XNAT data-type schemas and generates Java code, user interface elements, and properties files

Applying a particular plugin is as simple as directing Gradle to apply the plugin by name:

Applying plugins
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'xnat-data-builder'

The java and maven plugins are standard Gradle plugins and require no other configuration than specifying the names for them to be enabled for your build. The xnat-data-builder plugin is maintained by the XNAT team and published on our own artifact repository, so it requires a bit of extra configuration so that Gradle know where to find it. This is the buildscript block:

Required buildscript block for XNAT plugins
buildscript {
    repositories {
        mavenLocal()
        mavenCentral()
        jcenter()
        maven {
            url 'https://nrgxnat.jfrog.io/nrgxnat/libs-release'
            name 'XNAT Release Repository'
        }
        maven {
            url 'https://nrgxnat.jfrog.io/nrgxnat/libs-snapshot'
            name 'XNAT Snapshot Repository'
        }
    }
    dependencies {
        classpath "org.nrg.xnat.build:xnat-data-builder:1.7.2"
    }
}

Whereas the rest of your Gradle configuration configures how you want the build to be processed, the buildscript block basically configures Gradle itself. This tells Gradle where it can look for any libraries that it needs and also that it needs to download the xnat-data-builder plugin.

The block of code that starts with buildscript and is delimited by the '{' and '}' characters is called a closure. This post provides a nice explanation: "[A] closure is a function that can be stored as a variable (referred to as a 'first-class function'), that has a special ability to access other variables local to the scope it was created in."

Basically it's a function that gets passed to and called by another function. Everything between the '{' and '}' that delimits the buildscript closure gets passed to another function that will call it.

There are many other Gradle plugins available, including plugins to integrate your plugin project into your preferred development environment–eclipse for Eclipse, idea for IntelliJ IDEA), alternative languages (groovy, scala), test coverage (jacoco), and more. You can check out what's available on the Gradle Plugin Portal.

Specifying Java Compatibility

XNAT 1.7 is built to be compatible with Java 7 (also referred to as JDK 1.7). All libraries that want to work with XNAT, including XNAT plugins, also need to be compatible with Java 7. This doesn't mean that you have to use Java 7 to build your plugin: later versions of Java can easily build code that is compatible with earlier releases, but this needs to be specified at build time. In addition, there were small changes to the core Java APIs between Java 7 and Java 8 & 9. Even if you build Java 7-compatible code, these changes in API can cause problems when your code tries to find a method from Java 8 when running under Java 7. To make certain your plugin is compatible with Java 7, add the following configuration settings:

Making your plugin compatible with Java 7
sourceCompatibility = 1.7
targetCompatibility = 1.7

// For the next line to work, you need to add this as the first line (other than comments) of your build.gradle file:
// import org.gradle.internal.jvm.Jvm
def javaVersion = Jvm.current().javaVersion
if (javaVersion.java8Compatible || javaVersion.java9Compatible) {
    if (hasProperty("rt.17.jar")) {
        // Solution for bootstrap classpath warning and possible issues with compatibility with 1.7 libraries
        // was taken from this post on discuss.gradle.org: http://bit.ly/24xD9j0
        def rt17jar = getProperty("rt.17.jar")
        logger.info "Using ${rt17jar} as the bootstrap class path jar."
        gradle.projectsEvaluated {
            tasks.withType(JavaCompile) {
                options.fork = true
                options.compilerArgs << "-XDignore.symbol.file"
                options.bootClasspath = rt17jar
            }
        }
    } else {
        logger.warn "No value was set for the rt.17.jar build property, using the default bootstrap class path. You should consider setting rt.17.jar to indicate a jar file containing the Java 1.7 run-time library:\n"
        logger.warn "  ./gradlew -Prt.17.jar=rt-1.7.0_45.jar war\n"
    }
}

The sourceCompatibility and targetCompatibility settings are pretty straightforward: this tells the compiler that the Java code is written using Java 7-compliant code and that the compiler should in turn build Java 7-compliant byte code when it generates the class files for the Java source.

Everything after that is a little more complicated. This gets the version of your Java compiler (actually the JVM that's running the compiler, but it amounts to the same thing). If the compiler is compatible with Java 8 or 9, it checks to see if the property rt.17.jar has been specified. If it has, the Gradle project configuration is updated so that jar file is used as the bootstrap classpath jar, which defines the basic Java run-time library for the build. If the rt.17.jar property has not been defined, it will display a build warning.

In 99% of situations, you can leave out this entire section other than the sourceCompatibility and targetCompatibility settings. If you run into an error when you're running XNAT with your plugin that indicates a NoSuchMethodError for a method in the core Java libraries (most commonly with ConcurrentHashMap.keySet()), then you'll need to set up a 1.7-compatible run-time jar for compilation.

Configuring Source Sets

Gradle groups the code to be built into what it calls source sets. Each source set is usually associated with a particular plugin and/or build task. For example, both java and resources source sets are associated with the java plugin, but files in the java source set (located by default in src/main/java in your build folder) are compiled into Java class files, while files in the resources source set (located by default in src/main/resources in your build folder) are just copied into the build target. The xnat-data-builder plugin generates both Java code and resources from XNAT data-type schemas that it finds in the plugin project and places the generated code under the folder build/xnat-generated. The java plugin needs to know about this generated code as well as the code that it normally builds. To do this, you can just add extra folders to the standard source-set definitions:

Adding generated code to the Java source sets
sourceSets {
    main {
        java {
            srcDir 'src/main/java'
            srcDir 'build/xnat-generated/src/main/java'
        }
        resources {
            srcDir 'src/main/resources'
            srcDir 'build/xnat-generated/src/main/resources'
        }
    }
}

Adding Repositories for Dependency Resolution

As noted earlier, libraries are identified by most modern tools using three attributes, group ID, artifact ID, and version. Of course, you can have all of this information, but if you don't look in the correct repository, you still won't find the library you're looking for. The repositories closure specifies which repositories Gradle should search to find artifacts:

Adding repositories for dependency resolution
repositories {
    mavenLocal()
    mavenCentral()
    jcenter()
    maven {
        url 'http://dcm4che.org/maven2'
        name 'dcm4che Maven Repository'
    }
    maven {
        url 'https://nrgxnat.jfrog.io/nrgxnat/libs-release'
        name 'XNAT Release Repository'
    }
    maven {
        url 'https://nrgxnat.jfrog.io/nrgxnat/libs-snapshot'
        name 'XNAT Snapshot Repository'
    }
    maven {
        url 'https://nrgxnat.jfrog.io/nrgxnat/ext-release'
        name 'XNAT External Release Repository'
    }
}

The first repository specified, mavenLocal(), is a short-hand reference telling Gradle to look in the local Maven cache. This is generally located in the user's home directory in a folder named .m2. When Gradle downloads libraries from remote servers, it stores them in that local cache, so adding this reference directs Gradle to look there first, avoiding downloading the same libraries repeatedly.

The next repositories, mavenCentral() and jcenter(), specify two publicly available general-reference repositories. Most of the standard support libraries used by Java and web applications are hosted on these repositories.

The next repository, for dcm4che, hosts the libraries for the dcm4che framework. XNAT uses this framework for parsing and managing DICOM data and operations.

The remaining three repositories all point to XNAT-specific repositories. The first serves up the release versions of XNAT dependencies, the second serves snapshot or development releases, and the third serves up libraries from other developers that are necessary for XNAT to build and function but may not be available on other Maven repositories.

Specifying Dependencies

dependency is a library or resource that's required by another library or application in order to build or run properly. Your plugin will likely have at least a couple of dependencies if it has any Java code, including some XNAT-specific ones. There are two different types of dependencies:

  • Project dependencies are libraries that your code uses within its own algorithms
  • Build dependencies are libraries that need to be available as part of the build process itself.

Project Dependencies

Most of the time, your dependencies will be standard project dependencies. To declare a project dependency, you need a dependencies closure. The example below shows a fairly minimal set of dependencies, but this is still sufficient to build the code in xnat-template-plugin:

def vXnat = '1.7.2'
def vJunit = '4.12'
def vSpring = '4.2.9.RELEASE'
def vLog4j = '1.2.17'
def vSwagger = '2.4.0'

dependencies {
    compile("org.nrg.xnat:web:${vXnat}") {
        exclude group: '*'
    }
    compile("org.nrg.xnat:xnat-data-models:${vXnat}") {
        exclude group: '*'
    }
    compile("org.nrg.xdat:core:${vXnat}") {
        exclude group: '*'
    }
    compile "org.nrg:prefs:${vXnat}"
    compile "org.nrg:framework:${vXnat}"

    compile(group: 'turbine', name: 'turbine', version: '2.3.3') {
        exclude group: '*'
    }
    compile(group: 'org.apache.velocity', name: 'velocity', version: '1.7') {
        exclude group: '*'
    }

    compile "log4j:log4j:${vLog4j}"
    compile "io.springfox:springfox-swagger2:${vSwagger}"
}

Build Dependencies

We've already seen build dependencies earlier with the buildscript closure. This is something you'll need for every XNAT plugin build, because you need to reference the xnat-data-builder plugin within it. That said, you won't often need to do anything more than adding the xnat-data-builder plugin, unless you are building a plugin that itself relies on another plugin. For example, suppose you have a custom data type for your own type of imaging session and that's contained in one plugin. Then you decide to create another custom data type that extends your first one. You'd need to add the dependency for your first plugin in the dependencies within the buildscript closure for your new custom data type. If you don't, you would see compilation errors when trying to build the second custom data type, since the subclassed data type wouldn't be found.

Runtime Dependencies: Building a fat jar

If your code needs some dependency during runtime, not just at build time, and you know that XNAT does not include this dependency, then you will need to provide the code for that dependency along with your plugin. The easiest way to do that is to just copy the dependency's jar into the {{$\{XNAT_HOME\}/plugins}} directory alongside your plugin jar. For simple cases, this will work just fine and be totally sufficient.

However, that does not solve all cases. What if that dependency needs some other dependency? Then you'll have to track down multiple jars, and keep all of them together. And what if you want to distribute your plugin? It would be too much of a hassle to tell people how and where to get all the different dependency jars that your plugin needs. To solve those problems you can build a "fat jar", which is a jar that contains your code and all the dependencies it needs.

The first step is to add a new configuration to your build.gradle file called compileAndInclude:

configurations {
    compile.extendsFrom(compileAndInclude)
}

This creates the configuration. Now you can use that configuration instead of compile for any dependencies that you know you will want to include along with your code.

dependencies {
    compileAndInclude "io.rest-assured:rest-assured:3.0.1"
    compileAndInclude "com.jayway.jsonpath:json-path:2.2.0"

    compile("org.nrg.xnat:web:${vXnat}") {
        exclude group: '*'
    }
    compile("org.nrg.xnat:xnat-data-models:${vXnat}") {
        exclude group: '*'
    }
    compile("org.nrg.xdat:core:${vXnat}") {
        exclude group: '*'
    }
	...
}

So far the compileAndInclude configuration does not do anything different from the compile configuration. We can use it, though, for a new task that we call fatJar.

task fatJar(type: Jar) {
    zip64 true
    classifier = "fat"
    from {
        configurations.compileAndInclude.collect { it.isDirectory() ? it : zipTree(it) }
    } {
        exclude "META-INF/*.SF"
        exclude "META-INF/*.DSA"
        exclude "META-INF/*.RSA"
    }
    with jar
}

Now, instead of building the plugin with gradlew jar, we build it by running gradlew fatJar. This will create a jar containing your plugin, but also everything inside all the dependencies that you marked as compileAndInclude. It will be located in the same place as your ordinary jar, with the same name, except for a "-fat.jar" at the end.

Now you can deploy that single fat jar to XNAT, and it will contain everything it needs to run.

If you use the maven publishing plugin to publish your jar, you will also need to add the artifact to the publishing section for it to be picked up.

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
			...
			
            artifact fatJar


            ...
}