Writing your own TRIMM Java, TRIMM JPA or TRIMM Groovy/Grails generator extension
One of the major strengths of TRIMM Java and its submodules TRIMM JPA and TRIMM Groovy/Grails is that it is very easy to extend the code generator with new features without having to alter the TRIMM Products.
All you need to do is provide your own extension and hook it into TRIMM.
The code generation process and its hook points
The core generator is the dk.tigerteam.trimm.mdsd.java.generator.JavaGenerator
and all the submodules (dk.tigerteam.trimm.mdsd.jpa.JpaJavaGenerator
and dk.tigerteam.trimm.mdsd.groovy.generator.GroovyGenerator
) inherit their processing from it while providing their own instance of a dk.tigerteam.trimm.mdsd.java.generator.JavaCodeGenerationStrategy
– which again are specializations of the dk.tigerteam.trimm.mdsd.java.generator.DefaultJavaCodeGenerationStrategy
).
The dk.tigerteam.trimm.mdsd.java.generator.JavaCodeGenerationStrategy
allows you to fine tune the generation phase in even greater detail than what is described below.
The Java/JPA and Groovy/Grails code generation process
The JavaGenerator
handles the entire process of moving from a MetaModel
world to a CodeModel
world.
The configuration for the JavaGenerator is the dk.tigerteam.trimm.mdsd.java.generator.JavaGeneratorContext
instance which is passed to the #transform(JavaGeneratorContext)
or #transformAndWriteCode(JavaGeneratorContext, CodeWritingStrategy)
method, which kicks off the generation process.
These methods processes all MetaClazz
‘ in the MetaModel
during the following life cycle:
- FirstPassGeneration (
#firstPassGeneration(CodeModel, MetaModel)
)- First a corresponding
CodePackage
is created for eachMetaPackage
in theMetaModel
. - Then all
MetaClazz
are first pre-processed through the template methodJavaCodeGenerationStrategy#preProcessMetaClazz(MetaClazz)
. - After this the
MetaClazz
is run through theJavaCodeGenerationStrategy#shouldGenerateForMetaClazz(MetaClazz)
.
If this method returns true then
– AClazz
,Interface
orEnumeration
instance is created (depending on whether we are processing aMetaClazz
,
aMetaInterface
or aMetaEnumeration
).
– The generatedClazzOrInterface
(which is the common class forClazz
,Interface
andEnumeration
) is processed byJavaCodeGenerationStrategy#preProcessClazz(ClazzOrInterface, MetaClazz)
- First a corresponding
- Reconstruction of the inheritance hierarchy (
#reconstructClazzHierarchy(List)
) – After all classes have been created we perform the reconstruction of inheritance hierarchies forClazzOrInterface
according to how it’s defined in theMetaModel
- SecondPassGeneration (
#secondPassGeneration(List)
)
For eachClazzOrInterface
‘ generated in the#firstPassGeneration(CodeModel, MetaModel)
(and returned by
#reconstructClazzHierarchy(List)
) we now do the following:- Call either
#processClazz(Clazz)
(which create aClazzEvent
for bothMetaClazz
andMetaEnumeration
) or#processInterface(Interface)
(which create aInterfaceEvent
) and execute the event - This in turn calls either
#generateEnumerators(Enumeration)
forEnumeration
or
#generateFieldsAndProperties(ClazzOrInterface)
forClazz
‘ orInterface
#generateEnumerators(Enumeration)
will create and executeEnumeratorLiteralEvent
for eachMetaProperty
in theMetaEnumeration
that defined theEnumeration
#generateFieldsAndProperties(ClazzOrInterface)
will create and executePropertyEvent
subtypes (SimplePropertyEvent
or a {@AssociationEvent subclass such asOneToOneAssociationEvent
orOneToManyAssociationEvent
) for eachMetaProperty
defined in theMetaClazz
/MetaInterface
that defined theClazz
by delegating it to#createPropertyEvent(ClazzOrInterface, MetaProperty)
)#generateMethods(dk.tigerteam.trimm.mdsd.java.codedom.ClazzOrInterface)
is called. This will generate a
MethodEvent
for eachMetaOperation
defined in the meta model - Call either
- Finally it will run
DeferredEventHandler
‘ (#runDeferredEventHandlers()
) – At this point allClazzOrInterface
‘s are done, but certainGeneratorEventListener
(likeSerialVersionUIDGeneratorListener
and JPANamedTablesAndColumnsListener (from TRIMMJpa) might have needed allClazz
‘ andProperty
‘ to be created, before they can complete their event handling. - Postprocess Clazzes (
JavaCodeGenerationStrategy#postProcessClazzes(List)
)
This where the processing ends, unless you’ve called the#transformAndWriteCode(JavaGeneratorContext, CodeWritingStrategy)
method in which case the code is written to the destination using the providedCodeWritingStrategy
Generator Event hierarchy
As explained above most of the processing is done in an Event driven fashion, where the generator creates Events that it broadcasts to interested
dk.tigerteam.trimm.mdsd.java.generator.event.GeneratorEventListener
‘s.
Thedk.tigerteam.trimm.mdsd.java.generator.event.GeneratorEvent
them selves form a hierarchy that is displayed below:
GeneratorEventListeners
Most provided/built-in and customer specific code generator extensions are implemented as
dk.tigerteam.trimm.mdsd.java.generator.event.GeneratorEventListener
‘s.
dk.tigerteam.trimm.mdsd.java.generator.event.GeneratorEventListener
‘s used for a given Code generation are added to thedk.tigerteam.trimm.mdsd.java.generator.JavaGeneratorContext
instance which is passed to the#transform(JavaGeneratorContext)
or#transformAndWriteCode(JavaGeneratorContext, CodeWritingStrategy)
methods.Below is the complete list of
GeneratorEventListener
subclasses that ship with TRIMM Java, TRIMM JPA and TRIMM Groovy/Grails:
Configuring a JavaGenerator your self
If you don’t want to use our YAML driven CodeGenerators you can always choose to configure the JavaGenerator (or JPA/Groovy subclasses thereof) yourself like this:
MetaModel metaModel = new EAXmiReader().read(this.getClass().getResource("BuiltInTypesListener.xml")); JavaGenerator javaGenerator = new JavaGenerator(); JavaGeneratorContext generatorContext = new JavaGeneratorContext(metaModel); generatorContext.addEventListeners(new BuiltInTypesListener()); List<ClazzOrInterface> transformResult = javaGenerator.transform(generatorContext);
or if you want to write the generated code directly to the file system in Java code format
MetaModel metaModel = new EAXmiReader().read(this.getClass().getResource("BuiltInTypesListener.xml")); JavaGenerator javaGenerator = new JavaGenerator(); JavaGeneratorContext generatorContext = new JavaGeneratorContext(metaModel); generatorContext.addEventListeners(new BuiltInTypesListener()); List<ClazzOrInterface> transformResult = javaGenerator.transformAndWriteCode(generatorContext, new FileSystemCodeWritingStrategy(new JavaCodeWriter(...)));
Setting this up for JPA would basically require you to swap the
JavaGenerator
for theJpaJavaGenerator
and register one of the JPA specificGeneratorEventListener
‘s.For Groovy you would swap the
JavaGenerator
for theJpaJavaGenerator
and theJavaCodeWriter
for theGroovyCodeWriter
. Of course it would also make sense to register one or more of the Groovy/Grails specificGeneratorEventListener
‘s.Writing your own GeneratorEventListener
Basically all you need to do is to implement the
GeneratorEventListener
interface (either directly or using on the the convenience subclasses such asAbstractGeneratorEventListener
,AbstractJpaGeneratorEventListener
orBaseJpaGeneratorEventListener
).
All ourGeneratorEventListener
extend fromAbstractGeneratorEventListener
as it gives you a very simple way to handle only the specific Events that you want by overriding the designated method for that event and context.Here’s an example of how to do this:
public class BuiltInTypesListener extends AbstractGeneratorEventListener { private static final Log logger = LogFactory.getLog(BuiltInTypesListener.class); @Override protected boolean handleSimplePropertyEvent(SimplePropertyEvent event) { Type type = null; if (event.getMetaProperty().getCardinalityType() == CardinalityType.Many) { type = event.getProperty().getField() != null ? event.getProperty().getField().getType().getGenerics().get(0) : event.getProperty().getType().getGenerics().get(0); } else { type = event.getProperty().getField() != null ? event.getProperty().getField().getType() : event.getProperty().getType(); } MetaClazz metaClazz = event.getMetaProperty().getType(); resolveBuiltInTypes(event.getClazzOrInterface(), type); if (type.getClazzOrInterface() == null) { // Fix the meta model as well (serves later processes that use the built in types directly, without having the possibility of package rewriting, well) metaClazz.setName(type.getName()); } return true; } @Override protected boolean handleMethodEvent(MethodEvent event) { for (Parameter parameter : event.getMethod().getParameters()) { if (parameter.getMetaType().getType().isBuiltInType()) { resolveBuiltInTypes(event.getClazzOrInterface(), parameter.getType()); // Fix the meta model as well (serves later processes that use the built in types directly, without having the possibility of package rewriting, well) parameter.getMetaType().getType().setName(parameter.getType().getName()); } } if (event.getMethod().getMetaType().getReturnParameter().getType().isBuiltInType()) { resolveBuiltInTypes(event.getClazzOrInterface(), event.getMethod().getReturnType()); event.getMethod().getMetaType().getReturnParameter().getType().setName(event.getMethod().getReturnType().getName()); } return true; } /** * Override this method to provide custom resolving of built in types (resolving means fixing the {@link Type#getName()} property) * * @param type The type to resolve */ protected void resolveBuiltInTypes(ClazzOrInterface owner, Type type) { if (type.getClazz() == null && type.getWrappedJavaClass() == null) { logger.debug("Trying to resolve built in type '" + type.getName() + "'"); if (type.getName().equalsIgnoreCase("date")) { type.setName(Date.class.getName()).external(); } else if (type.getName().equalsIgnoreCase("bigdecimal")) { type.setName(BigDecimal.class.getName()).external(); } ..... else { throw new RuntimeException("Type '" + type + "' is unknown, please use" + " configuration 'mapUmlPropertyTypes' property to map custom types"); } } } }
Writing your own JPA generator extension
As mentioned above most JPA generators can benefit by extending either
AbstractJpaGeneratorEventListener
orBaseJpaGeneratorEventListener
.Here’s an example of how to write a JPA extension that inherits from
BaseJpaGeneratorEventListener
:public class Jpa2OrphanRemovalListener extends BaseJpaGeneratorEventListener { @Override protected boolean handleOneToOneOwnerOfAssociation(OneToOneAssociationEvent event) { if (isDeleteOrphanCandidate(event)) { Annotation annotation = event.getProperty().getField().findOrAddAnnotation(OneToOne.class); addOrSetOrphanRemovalAnnotationAttribute(annotation); } return true; } @Override protected boolean handleOneToManyOwnerOfAssociation(OneToManyAssociationEvent event) { if (isDeleteOrphanCandidate(event)) { Annotation annotation = event.getProperty().getField().findOrAddAnnotation(OneToMany.class); addOrSetOrphanRemovalAnnotationAttribute(annotation); // Remove the Set collection method since it allows for situations where Hibernate // (and perhaps other JPA implementations) fails, if you overwrite its internal collection wrapper event.getProperty().removeSetterMethod(); } return true; } protected boolean isDeleteOrphanCandidate(AssociationEvent event) { if (event.getMetaProperty().isOwnerOfAssociation() && !event.getMetaProperty().getAssociation().isBidirectional() && !event.getMetaProperty().getAssociation().isSelfReferencing()) { // Check the clazz of the opposite property to see what kind of associations it has for (MetaProperty subMetaProperty : event.getMetaProperty().getType().getProperties()) { if (subMetaProperty.isPartInAnAssociation()) { if (subMetaProperty.isOwnerOfAssociation()) { // One-To-One are always good for owning associations, so is Many-to-One, // so if we meet a ManyToMany or a OneToMany we fail it as a candidate for delete orphan if (subMetaProperty.getAssociationType() == AssociationType.ManyToMany || subMetaProperty.getAssociationType() == AssociationType.OneToMany) { return false; } } else if (subMetaProperty.getAssociation().isBidirectional()) { // The type of the our sub property is not an owning association and we have // a java association in both directions (bidirectional), which hibernate doesn't handle return false; } } } return true; } return false; } private void addOrSetOrphanRemovalAnnotationAttribute(Annotation annotation) { if (annotation.hasAnnotationAttribute("orphanRemoval")) { annotation.getAnnotationAttribute("orphanRemoval").setValues(true); } else { annotation.addAnnotationAttribute("orphanRemoval", true); } } }
Deferred Event handling
Sometimes you want to react to a given
GeneratorEvent
but you also want to wait until the entire CodeGeneration’s event handling has been completed in the SecondPassGeneration phase (i.e. no newGeneratorEvent
‘s will be fired by theJavaGenerator
), because your handling depends on information that isn’t at hand when the event arrives (e.g. when handling aClazzEvent
none of its properties or methods have been handled yet) or you want to wait and see if others have performed code changes that renders your event handling void.You can either explicitly
queue
deferred handling of a specific Event instance by callingqueueDeferredEventHandler(new DeferredEventHandler() { ... } )
on the currently activeJavaGeneratorContext
:
JavaGeneratorContext.getContext().queueDeferredEventHandler(new DeferredEventHandler() { ... } );
Note: The JavaGeneratorContext is bound to the currently running thread and can be fetched by callingJavaGeneratorContext.getContext()
or by calling the#getContext()
method from inside yourGeneratorEventHandler
Complete example where we defer handling of
ClazzEvent
andInterfaceEvent
since we want to wait and see if other event handlers introducedSerializable
into the code (hierarchy):/** * Makes sure that any given Clazz or Interface always implement {@link Serializable}.<br/> * The processing will occur deferred and will make sure that Serializable only will be applied if it isn't already a part of the * inheritance hierarchy * * @author Jeppe Cramon */ public class SerializablePojoListener extends AbstractGeneratorEventListener { @Override protected boolean handleClazzEvent(final ClazzEvent event) { JavaGeneratorContext.getContext().queueDeferredEventHandler(new DeferredEventHandler() { public String getDescription() { return "Serializable POJO for Clazz " + event.getMetaClazz(); } public void execute() { if (!implementsSerializable(event.getClazz())) { // Only add Serializable if we don't inherit from a Clazz or Interface if (!event.getClazz().extendsAGeneratedClazz() && !event.getClazz().implementsAGeneratedInterface()) { event.getClazz().addImplementsTypes(Serializable.class); } } } }); return true; } @Override protected boolean handleInterfaceEvent(final InterfaceEvent event) { JavaGeneratorContext.getContext().queueDeferredEventHandler(new DeferredEventHandler() { public String getDescription() { return "Serializable POJO for Interface " + event.getMetaInterface(); } public void execute() { if (!extendsSerializable(event.getInterface())) { // Only add Serializable if we don't inherit from a Clazz or Interface if (!event.getInterface().extendsAGeneratedInterface()) { event.getInterface().addExtendsTypes(Serializable.class); } } } }); return true; } private boolean implementsSerializable(Clazz clazz) { return clazz.implementsJavaInterface(Serializable.class); } private boolean extendsSerializable(Interface anInterface) { return anInterface.extendsJavaInterface(Serializable.class); } }
In this case we only handle
ClazzEvent
andInterfaceEvent
and not any of the other events, so here it makes sense to explicitly defer handling of only those events (it doesn’t make any sense to defer handling of events we don’t care about). We might have a case where we want to handle many events and in those cases handling the queueing explicitly can become very verbose. For those cases we offer theDeferredGeneratorEventListener
marker interface.
If your GeneratorEventListener implements this method, then all Events will automatically be queued for deferred handling:public class YourListener extends AbstractGeneratorEventListener implements DeferredGeneratorEventListener { protected boolean handleMethodEvent(MethodEvent event) { ... return true; } protected boolean handleInterfaceEvent(InterfaceEvent event) { ... return true; } protected boolean handleManyToManyAssociationEvent( ManyToManyAssociationEvent event) { return true; } protected boolean handleManyToOneAssociationEvent( ManyToOneAssociationEvent event) { return true; } protected boolean handleOneToManyAssociationEvent( OneToManyAssociationEvent event) { ... return true; } protected boolean handleOneToOneAssociationEvent( OneToOneAssociationEvent event) { ... return true; } protected boolean handleSimplePropertyEvent(SimplePropertyEvent event) { ... return true; } protected boolean handleEnumeratorEvent(EnumeratorLiteralEvent literalEvent) { ... return true; } protected boolean handleClazzEvent(ClazzEvent event) { ... return true; } }