The second part of the add-on implementation will focus on the operations to the entity classes. Recall that in an earlier post (
Association Add-on Design) we identified that the following functionality would be required.
- A association entity between the user provided entities.
- Annotation markup on each of the user provided entities.
- New annotation handlers that inject entity operations to manage the association.
Here we will be focused on responding to the users command to create a new association. This means building the functionality for the first two of these elements.
The final element, the annotation handles and the generation of the ITD's to manage the association will be covered in a subsequent post. Here we will be concentrating on the implementation contained in the AssociationOperationsImpl class.
Before jumping into code, I have been constructing a set of wrappers in another post,
'More On the Simplifying Development Of Addons'. that simplify defining the construction of the Java software elements. Please see that for help with the expressions.
The first method introduced to this class and main entry point for the shell is the newAssociation method. This method is required by the AssociationOperation interface. The method parameters represent the two JavaTypes that are being associated.
public void newAssociation(JavaType entity1, JavaType entity2 ) {
// Use Roo's Assert type for null checks
Validate.notNull(entity1, "Java type required");
Validate.notNull(entity2, "Java type required");
// new association entity is concatenation based on order
JavaType entityType = getAssociationEntityType(entity1, entity2);
buildAssociationEntity(entityType, entity1, entity2 ) ;
addEntityAnnotation( entity1, entity2 ) ;
addEntityAnnotation( entity2, entity1 ) ;
webAssociationOperations.newAssociation( entity1, entity2 ) ;
}
This code is pretty clean in expressing the implementation provided. It is reliant on three private method implementations that will need to be provided.
Get Association Entity
Recall from the design discussion we need a canonical association entity name. These entities determine if there is a relationship between the independent entities. And throughout the course of the design the treatment of the association entities is symmetrical. So the association of entities A to B is the same as B to A, It is important that the association entity table is the same in either case.
This method returns the canonical name of the association entity table. Since these names will always be a concatenation between the independent entity names we merely order those to provide the canonical nature required.
private JavaType getAssociationEntityType(JavaType target1, JavaType target2) {
if ( target1.getSimpleTypeName().compareTo( target2.getSimpleTypeName()) < 0 ) {
return new JavaType( target1.getFullyQualifiedTypeName() + target2.getSimpleTypeName()) ;
}
return new JavaType( target2.getFullyQualifiedTypeName() + target1.getSimpleTypeName()) ;
}
buildAssociationEntity
With the ability to create canonical association entity names we can proceed with implementing the buildAssociationEntity. The task of this method is to build if needed an JPA entity with the independent entities as attributes.
With Roo the add on code will be run multiple times, whenever upstream dependencies change. The Roo shell supports scripting, an I have in the past ran the same script over and over will developing it. So the implementation code should be defensive since the asset being built may already exist.
There are four main parts to this method;
- Attempt to obtain the ClassOrInterfaceTypeDetails for the association entity. To do this first we need to construct a metadataId and then attempt to get it from the type location service.
- Construct a ClassBuilder, we provide both the metadataId and our potentially null type details object. The constructor here will use the type details if available.
- Declare the annotations and attribute fields. The ClassBuilder handles the mechanics of updating the class structure.
- And finally we pass back the results of class builder to the TypeManagementService to handle committing the changes to the project files.
And here is the implementation;
private void buildAssociationEntity( JavaType entityType, JavaType target1, JavaType target2 ) {
final String metadataId = PhysicalTypeIdentifier
.createIdentifier(entityType, projectOperations
.getPathResolver().getFocusedPath(Path.SRC_MAIN_JAVA));
// Obtain ClassOrInterfaceTypeDetails for this java type
ClassOrInterfaceTypeDetails entityDetail = typeLocationService.getTypeDetails(entityType);
String identifierColumn = target1.getSimpleTypeName().toUpperCase()
+ "_" + target2.getSimpleTypeName().toUpperCase() + "_ID" ;
ClassBuilder classBuilder = new ClassBuilder( metadataId, entityDetail )
.type( PhysicalTypeCategory.CLASS )
.modifier( Modifier.PUBLIC | Modifier.FINAL )
.named( entityType )
.annotation( "org.springframework.roo.addon.javabean.RooJavaBean" )
.annotation(new AnnotationBuilder(
"org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord")
.attribute( "identifierColumn", identifierColumn ))
.annotation( "org.springframework.roo.addon.tostring.RooToString")
.field( getEntityAssociationField( metadataId, target1))
.field( getEntityAssociationField( metadataId, target2)) ;
typeManagementService.createOrUpdateTypeOnDisk(classBuilder.build());
}
getEntityAssociationField
The last part of the association entity construction is to add the fields to the type. For association entities there are two attribute (field) references. A reference is created for each of the entities being associated, so this method is called twice.
For each entity we need to add a field reference attribute to the entity association class to both ends of the association. Here the basic logic is first find or create a the reference field and then add the JPA annotations to the field.
private FieldBuilder getEntityAssociationField(
String metadataId, JavaType entityType ) {
ClassOrInterfaceTypeDetails entityDetails = typeLocationService.getTypeDetails(entityType);
AnnotationMetadata annotation = entityDetails.getAnnotation(
new JavaType( "org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord" )) ;
if ( annotation == null )
return null ;
String identifierColumn = (String) annotation.getAttribute( "identifierColumn").getValue();
if ( identifierColumn == null ) {
return null ;
}
return new FieldBuilder( metadataId )
.named(StringUtils.uncapitalize( entityType.getSimpleTypeName()))
.modifier( 0 )
.type( entityType )
.annotation("javax.persistence.ManyToOne")
.annotation( new AnnotationBuilder( "javax.persistence.JoinColumn" )
.attribute( "name", identifierColumn )) ;
}
At this stage we have finished construction of the association entity. This entity allows a many to many mapping between two independent entities.
addEntityAnnotation
Next we need to modify the independent entities that are being related together. Here we need to inject into the independent entities the association annotation. The association annotation identifies the other independent entities that this entity has been associated with. Recall during the design phase we recognized that an independent entity can have associations with multiple entities. This means that the value for the annotation will be a set of entity classes, but with Java annotations we need to provide a list.
To construct this set we will need to get the current annotation, if one exists, from the details object. If there is an existing annotation then exact the set of existing entities the class is associated with and remove that annotation. Next add the new entity class being associated with to the set of associated classes. And finally add (or re-add) the association annotation to the domain entity.
private void addEntityAnnotation( JavaType entityType, JavaType with ) {
// Obtain ClassOrInterfaceTypeDetails for this java type
ClassOrInterfaceTypeDetails entityDetails = typeLocationService.getTypeDetails(entityType);
if ( entityDetails != null ) {
// assemble the set of associated entities
Set<ClassAttributeValue> values = new HashSet<ClassAttributeValue>() ;
AnnotationMetadata associationMetadata = MemberFindingUtils.getAnnotationOfType(entityDetails.getAnnotations(), rooAssociationType) ;
if ( associationMetadata != null ) {
AnnotationAttributeValue<List<ClassAttributeValue>> attributeValue = associationMetadata.getAttribute( "value" );
values.addAll(attributeValue.getValue()) ;
}
values.add( new ClassAttributeValue( new JavaSymbolName( "value" ), with )) ;
ClassBuilder builder = new ClassBuilder( entityDetails )
.annotation( new AnnotationBuilder( rooAssociationType )
.attribute( "value", new ArrayList<ClassAttributeValue>( values ))) ;
// Save changes to disk
typeManagementService.createOrUpdateTypeOnDisk(builder.build());
}
}
In this method we see again the logic of;
- Obtaining the type details from the TypeLocationService,
- Wrapping the type details in a ClassBuilder facade object,
- Modifying the builder,
- And finally sending the updated class to the TypeManagementService.
This wraps things up for the implementation of the domain model handling for the association plug in. Roo web applications follow the basic model view controller architecture. Add on constructed in this post provides the association functionality to the model component. In the next post we will look at the implementation for the controller portion of the architecture.