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 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.
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:
The advantages of using preferences include:
To store data in the configuration service, you need to:
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:
@Component
public class MyService {
@Autowired
public MyService(final ConfigService configService) {
_configService = configService;
}
}
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:
public class MyService {
public MyService() {
_configService = XDAT.getConfigService();
}
}
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:
Property | Description |
---|---|
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. |
Scope | 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. |
Entity ID | |
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. |
@Component
public class MyService {
@Autowired
public MyService(final ConfigService configService) {
_configService = configService;
}
public void storeMyServiceConfiguration(final String configuration) {
_configService.replaceConfig(_user, "I want to", "myService", "primary", configuration);
}
}
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.
Scope | Attribute | Description |
---|---|---|
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:
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.