When building a new service to integrate with XNAT, it's common early on to need to store preferences of some sort: a setting for how often a process should run, storing the address of a server where data storage or retrieval, a script or command for launching an external process. XNAT 1.7 provides different services for managing these settings, as well as an application framework that makes it easy to create your own library of preference settings with just a small amount of Java code.
Configurations vs Preferences
It's important to understand the difference between configurations and preferences so that you can use the one that's appropriate for the type of data you want to store.
A configuration in XNAT is a semantics-free text resource. That is, the configuration service doesn't care if the text in a particular configuration is JSON, a DicomEdit anonymization script, a chunk of Python code, or a copy of the Gettysburg Address. The content of the configuration is completely dependent on the code or service that stores and consumes the configuration. In this regard, the configuration service is pretty simple: store some text, give it a name, and retrieve the configuration whenever you need it. The advantage of using the configuration service is precisely in the lack of semantics applied to what you store in it. There's no interpretation, validation, transformation, or any other attempt to restrict what you put into it or to modify what you retrieve from it. This makes the configuration service a convenient place to hunks of configuration data. The drawback of the configuration is the same as its advantage: there are no convenience methods for extracting particular values or options from your stored configuration or for converting the contents of, e.g., a JSON configuration to a map or instance of a Java class.
The XNAT preferences service provides something like a key-value store:
- Each individual preference setting can be identified by name
- Multiple preferences can be grouped by tool
- Multiple values can be set for a single preference, distinguished by the scope to which a particular value applies
- The preferences service provides extensive support for converting values from native Java types (e.g. strings, numbers, lists, maps, etc.) to a serializable form that can be stored in the database and restoring serialized values back to an instance of the original Java type
The advantages of using preferences include:
- Value atomicity: you can change a single preference value for a particular tool without affecting the other values. Compare this to configurations: you can't update one small part of a configuration without updating the entire configuration.
- Scoping: this lets you define a preference at one level then override it at other levels. For example, think about setting a preference for something like the user theme. You would first set a site-wide value for this preference. When users log in, the theme service would check for the theme preference value for each user. If there's no value set for the preference scoped to a particular user, the preference service "fails up" to the next level in the hierarchy, eventually getting to the default site-wide value. When a user overrides the theme value, the preferences service stores the same preference name with the custom value, but scoped to user A's ID. When any user that hasn't overridden the theme preference logs in, the preference service gets the site-wide setting, but those who do override the value get their own setting.
- The ability to store simple Java objects, including all standard Java primitives and common storage classes (e.g. lists, maps, etc.), in the database without having to also configure services, database access, object serializers and deserializers, and so on.
Storing Data in the Configuration Service
To store data in the configuration service, you need to:
- Get a reference to the ConfigService instance
- Create configurations
- Retrieve configurations
Getting the ConfigService Instance
Storing data in the XNAT configuration service requires a reference to the ConfigService bean, as all of your configuration transactions–creating a new configuration, updating an existing configuration, or deleting a configuration–goes through the configuration service.
In Spring-based classes, you can autowire the bean to your services. For example:
In non-Spring classes like screen or action classes for Turbine actions or Velocity displays, you can access the service through the XDAT.getConfigService() method:
Creating and Updating Configurations
Once you have a reference to the configuration service, you can create, retrieve, and update configurations. Configurations are actually managed through the Configuration class, which has the following properties:
|Tool||Indicates the tool with which the configuration is associated. This is just a string, but lets you associate multiple configurations through its value.|
|Path||Identifies the particular configuration for the tool. By convention, this can be specified as a path separated by slashes, providing a further level of conceptual hierarchy for related configurations. In practice, this just needs to be a valid string.|
The scope and entity ID properties are used together to specify a particular object to which a configuration applies:
Refer to Understanding Scope and Entity IDs for more detail.
|User||A reference to the user who updated a particular version of a configuration.|
|Reason||A user-provided reason for why the configuration was updated.|
|Status||The current configuration status. The only valid statuses are "enabled" and "disabled".|
|Version||The current version of the configuration.|
|Unversioned||This is a boolean property: when true, the configuration is changed when updated, but when false any changes or updates result in a new instance of the configuration with the changes becoming the latest current version of the configuration.|
Storing Settings in the Preferences Service
Managing Settings with Preferences Beans
Understanding Scope and Entity IDs
Before getting into specifics about the different ways of storing settings in XNAT, you should understand how XNAT identifies objects–or entities in this context–across the system. Every entity in XNAT, whether it's system data or a core XNAT data object, has a unique ID that distinguishes it on the system, but only from other objects of the same type. It would be possible, for example, for a project and an MR session and a user to have the same "unique" ID. Since the IDs aren't guaranteed to be unique across the system, identifying a particular object or resource requires the addition of scope, which defines the context in which the ID is unique. The combination of these two properties, scope and entity, is called the entity ID. The table below describes some sample entity IDs with the scope and object attribute that uniquely identifies the type of the entity.
|Project||Id||The id attribute on a project uniquely identifies the project with the scope of other projects.|
|User||login||The user table in XNAT also has a more standard numeric unique identifier (xdat_user_id), but that's not very readable for humans, so users are usually defined by the user's login name.|
|Experiment||id||As with projects, experiments' id attribute uniquely identifies the experiment from other experiments.|
|Site||None||The site scope is unique in that it's the very top of the object hierarchy in XNAT. There is only one site, so there's no need for an ID to distinguish it from other entities.|
Identifying a particular entity within the system allows you to apply a configuration or preference to that entity. This is useful, but becomes quite powerful within the context of entity resolution. This is the idea that a particular entity doesn't exist just on its own, but within a hierarchical structure of related data objects or even within multiple hierarchical structures simultaneously. This lets you apply settings or configurations at different levels in a hierarchy, setting defaults at a higher level and overriding or adding to those defaults at lower levels. How entities are resolved, what hierarchy is used, and whether settings override or add to settings at other levels depends on the particular tool and implementation.
To illustrate how this works, there are different implementations of this concept in the core XNAT implementation:
- Anonymization scripts describe how to modify the values of DICOM headers to remove PHI and sensitive personal and health data. Many XNAT sites have a site-wide anonymization script that performs some general operations to clean up the data, then have project-specific anonymization scripts that can be more specific in their operations because, for example, those projects receive data from a particular scanner that puts patient data into a particular set of fields. Here the hierarchy is fairly simple and consists of projects and then the site. The settings are additive: first the site-wide and then the project-level anonymization scripts are applied.
- Preferences in core XNAT can be applied at any level from a particular experiment, up to the subject with which experiments are associated, then to the project that contains the subjects, and then to the site level. As a practical matter, individual preference settings are almost never applied at the experiment or subject level, but it is possible. When checking for value of a preference setting, XNAT starts at the level to which it's trying to apply the setting, so when displaying an MR session, it checks for a setting for that preference associated with the session's entity ID. If it doesn't find a value there, it "fails up" and checks for a setting for that preference for the subject, then the project, and finally the site until it finds a value. This is an example of where the preference or configuration at a particular level in the hierarchy overrides the value at another level.
While it's possible to define and implement your own entity resolution scheme, this section just explains how scope and entity ID work to provide the context for understanding the configuration and preferences services.