Step 5 of 10 Configuring new services
Goal
In this step, you'll create a new service that you can use to manage data outside of the standard XNAT data domain.
XNAT data types provide a lot of power for handling data within the application's primary domain of research data. Along with this power, however, comes a fair amount of overhead to create and maintain the data types. For data outside XNAT's core functionality, you can create a Java-based service that provides its own persistent data types and even its own discoverable and documented REST API.
XNAT's data services and REST API use the Spring Framework and Hibernate data persistence framework. The plugin framework provides a number of ways you can integrate your own code into XNAT's own Spring and Hibernate implementations. This step of the practical exercise will focus primarily on how to accomplish this integration and not on details of how to write Spring and Hibernate code. Where appropriate, we will provide links to sources for more information on particular aspects or features of these frameworks.
For this exercise, suppose you'd like to have a way to map your research subjects back to subject records stored in other systems, whether those systems are other research tools such as REDCap and OpenClinica or a clinical system such as a hospital's PACS or patient record system. The simplest implementation of this system could associate the ID of the subject in your XNAT system with the source system and ID within that system of the XNAT subject. This requires the following classes in your implementation:
- An object to hold the data for an individual subject. This is the data entity class.
- A service layer that can create, retrieve, update, and delete your data entities (the so-called CRUD operations). This includes the service and repository classes.
- An interface that lets you control the data service to manage your data entities. This will be a REST API implementation.
Let's look at each of these in turn.
The classes discussed on this page are provided in the attached zip file. If you'd prefer to just follow along, you can unzip this file in the root folder of your plugin project. The extracted files can be found under the folder src/main/java/org/nrg/xnat/workshop/subjectmapping.
Data entity
A data entity class should be, as much as possible, nothing but a container for data. It provides no functionality, no operations, no means of transformation. It is simply a noun, a thing, a collection of properties. The subject mapping entity, then, comes down to three properties, the subject ID, the source system, and the ID of the subject within the source system. In Java, this would look something like this:
Subject mapping data entity
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "subjectId"),
@UniqueConstraint(columnNames = {"recordId", "source"})})
public class SubjectMapping extends AbstractHibernateEntity {
public String getSubjectId() {
return _subjectId;
}
public void setSubjectId(final String subjectId) {
_subjectId = subjectId;
}
public String getRecordId() {
return _recordId;
}
public void setRecordId(final String recordId) {
_recordId = recordId;
}
public String getSource() {
return _source;
}
public void setSource(final String source) {
_source = source;
}
private String _subjectId;
private String _recordId;
private String _source;
}
How does an instance of this class get stored in the database? In the context of the entity class, it doesn't really matter. That's the work of the service layer. This class only provides a little bit of information that even indicates that it's supposed to be a data entity:
- The @Entity annotation indicates that this is an entity class. This annotation is meaningful to a number of Java persistence frameworks, including Hibernate.
- The @Table annotation and the @UniqueConstraint annotations it contains are basically advice to persistence frameworks indicating that there should only be a single entry in the table for each subject ID and only a single entry for each combination of source system and record ID.
- The AbstractHibernateEntity class that this entity extends is an NRG framework class that provides a bunch of standard properties that are useful for objects stored in the database. It also makes it so that this object can be managed by the NRG framework entity management classes.
Everything else about storing and retrieving the data in one of these classes is handled at the service layer, so let's look at this next.
Service layer
In the XNAT context, the service layer actually consists of two parts:
- The data access object (DAO) or repository (DAO and repository are used basically interchangeably) performs atomic database operations, handles basic framework details like session and state management, and implements specific low-level operations like detailed queries and joins.
- The service implementation performs business or domain operations, which combine one or more database operations into a single transaction.
This model provides a lot of power. The most important aspect is the division of individual operations at the repository level and aggregate operations at the transaction level. By making these aggregate operations transactional, an error in any atomic operation at any point in the aggregate operation can result in the entire operation being rolled back, so that your database is protected from partial operations that can cause issues with data integrity.
Let's start with the repository implementation. The NRG framework provides a base implementation named AbstractHibernateDAO. If you create a class that extends this base class, you get all of the basic data operations–create, retrieve, update, delete, and a variety of find operations including the ability to find by example and by or more more properties–without writing any more code than just declaring your class, like so:
Subject mapping repository
@Repository
public class SubjectMappingRepository extends AbstractHibernateDAO<SubjectMapping> {
}
The @Repository annotation indicates that this is a data repository. We'll see how this is utilized when we discuss component scanning below. Also, notice that the SubjectMapping class is referenced here. When we added the extends clause to the SubjectMapping class, that made it an entity that your repository understands and knows how to manage.
The NRG framework provides a base implementation for service classes as well. For various technical reasons, you should divide your service classes into an interface definition and an actual implementation. The interface definition specifies the operations available through the service.
Subject mapping service interface
public interface SubjectMappingService extends BaseHibernateService<SubjectMapping> {
SubjectMapping findBySubjectId(final String subjectId);
SubjectMapping findByRecordId(final String recordId, final String source);
List<SubjectMapping> findBySource(final String source);
}
Note that this interface extends another interface and that is also configured so that your interface is associated with the SubjectMapping entity. This interface defines some of the basic operations you'd like to be able to perform with this service, including finding a subject by its XNAT ID, finding a subject by the record ID, which requires the source system as well, and finding all of your subjects that originate from a particular system.
Now you just need to implement this service:
Subject mapping service implementation
@Service
public class HibernateSubjectMappingService
extends AbstractHibernateEntityService<SubjectMapping, SubjectMappingRepository>
implements SubjectMappingService {
@Transactional
@Override
public SubjectMapping findBySubjectId(final String subjectId) {
return getDao().findByUniqueProperty("subjectId", subjectId);
}
@Transactional
@Override
public SubjectMapping findByRecordId(final String recordId, final String source) {
final Map<String, Object> properties = new HashMap<>();
properties.put("recordId", recordId);
properties.put("source", source);
final List<SubjectMapping> subjectMappings = getDao().findByProperties(properties);
if (subjectMappings == null || subjectMappings.size() == 0) {
return null;
}
return subjectMappings.get(0);
}
@Transactional
@Override
public List<SubjectMapping> findBySource(final String source) {
return getDao().findByProperty("source", source);
}
}
Important things to notice about this implementation include:
- The @Service annotation indicates that this is a service implementation. Like the @Repository annotation, we'll discuss how this comes into play when we look at component scanning.
- This implementation extends the AbstractHibernateEntityService, which provides implementations for the basic operations defined in the BaseHibernateService interface.
- It also associates itself with both the SubjectMapping entity class and the SubjectMappingRepository class. The service implementation now understands how to work with both of these classes.
- Each method in this implementation is annotated with the @Transactional annotation. It's this annotation that tells the database transaction manager where the business operations begin and end, so that if an error occurs inside that transactional operation, the overall operation can be rolled back.
Completed!
You've defined a new data entity class and the service and repository required to manage the data.
Go to the next step
Programming XNAT