In Candlepin 2.2, we’ve begun separating our model entities from actual DTOs (data transfer object) to be returned by the various object mappers used throughout the code base. This gives us more flexibility in changing the backend data model without impacting API, event or manifest consumers.
Use the following criteria as a guideline for when a DTO should be used:
Applying and using the DTOs is pretty straight forward, as they don’t contain any special logic or require setup. Which DTOs should be used, however, is context-specific. If the DTO is planned to be sent to a consumer, then the DTO from the appropriate package should be used. At the time of writing, there are four main packages of DTOs, to cover various consumers:
candlepin.dto.api
Contains DTOs for entities used as input and/or output through the Candlepin API (resources)
candlepin.dto.rules
Contains DTOs for entities sent or received from the Javascript rules
candlepin.dto.manifest
Contains DTOs for entities written to, or read from a manifest
candlepin.dto.shim
Contains DTOs and tools used to translate between legacy and deprecated objects and the newer DTO framework
Once the proper DTO package has been selected, a DTO can be instantiated in one of two ways: either by instantiating and populating it directly as per standard programming practices, or by “translating” an existing model entity.
It’s important to note that DTOs should stay within the realm in which they are intended. For example, a DTO from the API package should not be used in the adapters, passed in through the rules, or used in manifest processing. Such a DTO should be restricted to the API resources, controllers and directly utility classes explicitly for API processing.
The easiest way of converting a model entity to a DTO is to use a model translator to do the work for you. A
ModelTranslator
is a mapper that manages ObjectTranslator
instances and determines which
translators to use for a given translation task. To convert an entity with this method, we’ll need a model
translator and one or more object translators.
A basic model translator implementation is the SimpleModelTranslator
, which implements the standard
functionality defined by the model translator interface. We can use this implementation for registering the
object translator(s) we’ll be using.
ModelTranslator modelTranslator = new SimpleModelTranslator();
Next we need some object translators to handle the actual type to type translation. For this example, we’ll
translate Owner
objects to OwnerDTO
objects, for which we can use an OwnerTranslator
.
OwnerTranslator ownerTranslator = new OwnerTranslator();
All we need to do is register the owner translator as a handler for the Owner to OwnerDTO translation:
modelTranslator.registerTranslator(ownerTranslator, Owner.class, OwnerDTO.class);
Finally, with the translators in place, we can translate our owner entities by using the .translate
method:
Owner entity = <fetch owner entity from database>
...
OwnerDTO dto = modelTranslator.translate(entity, OwnerDTO.class);
At this point, the dto is fully populated with data from the owner entity, and it’s nested entities where appropriate and necessary.
Even easier than converting using the model translator method described above is to simply use the standard
translator. The StandardTranslator
is a pre-defined and configured translator that contains translations
for all supported model entities straight out of the box. This can be instantiated directly, or injected by
the dependency injection framework.
Direct instantiation:
ModelTranslator modelTranslator;
...
this.modelTranslator = new StandardTranslator(...);
Dependency injection:
@Inject
public MyResource(ModelTranslator modelTranslator) {
...
this.modelTranslator = modelTranslator;
...
}
Note that when we use dependency injection, we’re injecting it as a generic ModelTranslator
rather than
the more specific StandardTranslator
Once the translator is created, simply call the .translate
method as we did in the previous example:
Owner entity = <fetch entity from database>
...
OwnerDTO dto = this.modelTranslator.translate(owner, OwnerDTO.class);
At the time of writing, the model translator does not support true bulk translation. However, it does expose a method for use with the Java 8 streaming API to process collections of objects.
The method in question is the .getStreamMapper
, which returns a mapper method to be used with the
.map
intermediate stream operation.
Collection<Owner> entities = <fetch entity collection from database>
Stream<OwnerDTO> dtoStream = entities.stream()
.map(this.modelTranslator.getStreamMapper(Owner.class, OwnerDTO.class);
From this point, the stream can be iterated or terminated as normal. This is preferable to manual iteration and translation using a loop, as it avoids the object translator lookup on every iteration.
Designing a new DTO within this DTO framework is a three step process:
org.candlepin.model
Java package.org.candlepin.model
Java package.When creating DTOs for the new DTO framework, a number of design requirements should be followed to ensure consistency and stability across the entire framework. The following requirements are listed in no particular order, and should all be strictly followed, excempting only the most explicit and obscure circumstances.
Once a DTO has been created in accordance with the requirements above, the next step is to create the necessary translators that will process or output the new DTO.
For each translation that will use this DTO, a translator implementing the ObjectTranslator
interface,
typed with the input and output class of the translation. For example, the translator which handles owner to
owner DTO translation implements the interface: ObjectTranslator<Owner, OwnerDTO>
.
The ObjectTranslator
interface defines four methods of two classes: translate
and populate
.
The translate methods take a source object and output a new instance representing the translated object.
Similarly, the populate methods take a source object and a destination object, and update the destination
object with data from the source object. Both methods have a overloaded definition which also accepts a
model translator which can be used to translate nested objects within the source object.
The overlapping nature of the translate and populate operations allows most object translator implementations to simply chain the translate operation into the populate operation rather than duplicate code. For example:
@Override
public OwnerDTO translate(Owner source) {
return this.translate(null, source);
}
/**
* {@inheritDoc}
*/
@Override
public OwnerDTO translate(ModelTranslator translator, Owner source) {
return source != null ? this.populate(translator, source, new OwnerDTO()) : null;
}
/**
* {@inheritDoc}
*/
@Override
public OwnerDTO populate(Owner source, OwnerDTO destination) {
return this.populate(null, source, destination);
}
/**
* {@inheritDoc}
*/
@Override
public OwnerDTO populate(ModelTranslator translator, Owner source, OwnerDTO dest) {
// Do actual work here
}
This leaves the populate method to handle the bulk of the menial copying work, without needing to worry about duplicating the logic in multiple places.
As mentioned above, for objects which contain nested objects, the provided model translator can be used to offload the translating of these nested objects back to the appropriate translator. For instance:
if (modelTranslator != null) {
Pool pool = source.getPool();
dest.setPool(pool != null ? modelTranslator.translate(pool, PoolDTO.class) : null);
}
else {
dest.setPool(null);
}
This is optional behavior, and should default to null in cases where processing nested objects is either not provided, or when a model translator is not available.
Finally, once the translators are complete, they should be added to the standard translator. At the time of
writing, this is a matter of simply updating the default constructor of the StandardTranslator
to
register the new translator during instantiation:
this.registerTranslator(new MyObjectTranslator(), InputObject.class, OutputObject.class);
This will translation of the input object via the standard translator, without any additional work on the part of developers who will be using it.