Groovy Traits and Sling Models in AEM

Using sling models to inject state and default logic in traits.

A little while back Paul Michelotti wrote a great blog showcasing the benefits of using Groovy Traits along with the CQ Component Plugin. Traits can be used for multiple inheritances in Groovy, but unlike interfaces, they can contain state and default logic. However, I wanted to take it a step further and use Sling Models for injecting that state. In this blog I will go through what additional steps need to be taken in order to get Sling Models to work.

First, lets take a look at the component class from Paul’s example. The component has a couple properties of its own but also inherits more from two separate Groovy traits.

@Component(value = "Trait Composed Component")
class TraitComposedComponent extends AbstractComponent implements Categorizable, Authorable {
 
   @DialogField(fieldLabel = "Title", ranking = -1000D)
   public String getTitle() {
       get("title", "Title")
   }
 
   @DialogField(fieldLabel = "Content", ranking = -990D)
   @RichTextEditor
   public String getContent() { 
       get("content", "Content") 
   }
 
}

If you refer back to Paul’s blog you will see the code for initializing those trait variables is a bit complex for basic Sling Model injection.  Of course, you could create custom injectors, but that is outside the scope of this blog.  Therefore, lets use something else for demonstrating how to utilize Sling Models.  Take that “title” field, for instance.  You might have several components that all have a title.  We could break that out into yet another trait.

trait Titled implements Component {
    
    @Inject
    @DialogField(fieldLabel = "Title", ranking = -1000D)
    String title

}

Now we have extracted the “title” property out of TraitComposedComponent and put it in a new Groovy trait called Titled. Of course, you will want to add Titled into your list of implemented traits in the component class. We can also go ahead and use Sling Models for the “content” variable, since it is a simple String, and remove the getter. Now we end up with something like this:

@Model(adaptables = Resource)
@Component(value = "Trait Composed Component")
class TraitComposedComponent extends AbstractComponent implements Classifiable, Authorable, Titled {

    @Inject
    @DialogField(fieldLabel = "Content", ranking = -990D)
    @RichTextEditor
    String content

}

That should be all there is to it, right? Let’s try to compile what we have.

[ERROR] Failed to execute goal org.apache.felix:maven-scr-
plugin:1.20.0:scr (generate-scr-scrdescriptor) on project 
cq-tcc-core: Execution generate-scr-scrdescriptor of goal 
org.apache.felix:maven-scr-plugin:1.20.0:scr failed: A 
required class was missing while executing 
org.apache.felix:maven-scr-plugin:1.20.0:scr: 
com/yourcompany/your/package/Titled$Trait$FieldHelper$1

Looks like we got a compile error. Groovy does some crafty tricks during the compilation of traits, such as generating extra classes like the one seen in the error. As you can see, the problem is with the SCR Maven Plugin. If we check our POM file for the SCR configuration we will most likely find that “scanClasses” is set to true. Since we are not using SCR annotations in these traits, go ahead and exclude Titled to avoid the error.

<plugin>
  <groupId>org.apache.felix</groupId>
  <artifactId>maven-scr-plugin</artifactId>
  <extensions>true</extensions>
  <executions>
    <execution>
      <id>generate-scr-scrdescriptor</id>
      <goals>
        <goal>scr</goal>
      </goals>
      <configuration>
        <scanClasses>true</scanClasses>
        <excludes>
           com/yourcompany/your/package/Titled*
        </excludes>
      </configuration>
    </execution>
  </executions>
</plugin>

Try compiling again.

[ERROR] Bundle com.citytechinc.cq:cq-tcc-core:bundle:0.0.2-
SNAPSHOT : The default package '.' is not permitted by the 
Import-Package syntax. 
This can be caused by compile errors in Eclipse because 
Eclipse creates valid class files regardless of compile errors.
The following package(s) import from the default package 
[com.yourcompany.your.package]
[ERROR] Error(s) found in bundle configuration

Another error. Some more of the Groovy magic is getting in the way of our traits getting compiled. In this case, we unfortunately have to sacrifice the awesomeness that is implicit getters in Groovy. Change the access modifier for “title” to explicitly be private and then add a getter.

trait Titled implements Component {

    @Inject
    @DialogField(fieldLabel = "Title", ranking = -1000D)
    private String title

    public String getTitle() {
        title
    }

}

Once again, we try compiling. Finally, we get a successful build. Let’s go take a look at the component that was created in the repository.

Groovy Repository

Everything is there except, instead of a node of “titled”, we see it has created a node with a long drawn out name. To fix this we will want to explicitly specify a “fieldName” in the DialogField annotation of the “title” property.

trait Titled implements Component {

    @Inject
    @DialogField(fieldLabel = "Title", fieldName = "title", ranking = -1000D)
    private String title

    public String getTitle() {
        title
    }

}

After recompiling and deploying again we find that the node name has now been corrected.

Groovy Repository2

However, if we take a look at the name property of the “title” node we will see a similar issue with that.

Nodes

This time we need to add an explicit “name” property in the DialogField annotation and set it to “./title”.

trait Titled implements Component {

    @Inject
    @DialogField(fieldLabel = "Title", fieldName = "title", name = "./title", ranking = -1000D)
    private String title

    public String getTitle() {
        title
    }

}

If we go back and look at the properties of the “title” node after another build we will see that the “name” field has been fixed.

Groovy Nodes2

So, the component looks to be defined the way it should be. Let’s add a component to a page and fill in edit dialog.

Trait Composed Component

When the page reloads we can see the author, but Title is not showing.

Trait Composed Component 2

Because of the Groovy magic that is happening, Sling Models also needs to be told exactly what the component property names are to inject. To do this, we add @Named annotation to the “title” property in the trait.

trait Titled implements Component {

    @Inject @Named("title")
    @DialogField(fieldLabel = "Title", fieldName = "title", name = "title", ranking = -1000D)
    private String title

    public String getTitle() {
        title
    }

}

Once we build and reload the page we see that the “title” and everything else is displaying as expected.

Trait Composed Component 3

And there you have it. We are now taking advantage of Groovy Traits for multiple inheritance, as well as Sling Models for injection. It takes a bit of extra effort but as you can see, it is possible.