AEM is a robust platform full of useful APIs and frameworks available at our disposal. Understanding what’s in the box will help us write less code. In my previous blog post, I covered one of the most used frameworks, Sling Models. Plus, I showed a real-world example with multi-fields. For this blog post, we will continue to leverage Sling Models with added help from a code generator.
Code Generators
Code generators save us from having to write monotonous Java plumbing code. Things like bean getters and setters, loggers, equals(), toString() and hashCode(). We need to free ourselves from having to write and maintain this plumbing. In doing so, we can focus on high-level business logic.
Some of the better-known generators are Immutables, AutoValue and Lombok. I’m not going to get into the details of each. Suffice it to say that after a few years and a few projects my personal preference is Lombok. Like the other two, it can generate value objects. But it does so much more than that.
javac
, the Java compiler, executes code generators. Like all things in Java, there is a JSR. In this case JSR 269: Pluggable Annotation Processing API. Since we are using Maven, all we need to do is add the Maven dependency
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>provided</scope> </dependency>
As if by magic, all your Lombok annotated classes will get augmented with generated code. Well… not magic. Javac will find and load META-INF/services/javax.annotation.processing.Processor
. This file resides in lombok.jar
and points to the appropriate annotation processor. You can read a more detailed explanation here. It is so seamless that the first time I pulled down a project built with Lombok I was not even aware of its presence. Not until I ran into an annotated class. That is on the compiler side. On the IDE side, the latest version of IntelliJ is already compatible with Lombok. Eclipse requires some setup.
A Real-World Example
At some point, every AEM developer has had to add classes to the <body>
tag. Sounds simple right? We will add one constraint: properly extend the WCM Core Page Template Component. The kneejerk reaction would be to overwrite the page.html file where the <body>
tag is. I did say “properly extend” right? That file has a lot of things in it to support Adobe’s platform & features. If we overwrite, then we would become responsible for keeping it up to date between upgrades.
The <body>
classes are coming form the page model com.adobe.cq.wcm.core.components.models.Page. The proper way to extend the core model is through the ResourceSuperType, a Via provider type. Here is a more detailed example of how to extend core components.
Let’s fire up a new archetype project:
mvn -B archetype:generate \ -D archetypeGroupId=com.adobe.aem \ -D archetypeArtifactId=aem-project-archetype \ -D archetypeVersion=26 \ -D appTitle="My Site" \ -D appId="mysite" \ -D groupId="com.mysite" \ -D aemVersion=6.5.5
And the first class we will create is com.mysite.core.models.MyPageModel
@Model(adaptables = { SlingHttpServletRequest.class }, adapters = { Page.class, ContainerExporter.class }, resourceType = "mysite/components/page") @Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION) public final class MyPageModel implements Page, ContainerExporter { private static final Logger log = LoggerFactory.getLogger(MyPageModel.class); @Self @Via(type = ResourceSuperType.class) private Page delegate; @Override public String getCssClassNames() { log.info("this is the only relevant change"); return String.join(" ", "foo", "bar", this.delegate.getCssClassNames()); } /** Evertying below this is plumbing. */ @Override public String getLanguage() { return this.delegate.getLanguage(); } @Override public Calendar getLastModifiedDate() { return this.delegate.getLastModifiedDate(); } @Override public String[] getKeywords() { return this.delegate.getKeywords(); } @Override public String getDesignPath() { return this.delegate.getDesignPath(); } @Override public String getStaticDesignPath() { return this.delegate.getStaticDesignPath(); } @Override public String getTitle() { return this.delegate.getTitle(); } @Override public String[] getClientLibCategories() { return this.delegate.getClientLibCategories(); } @Override public String[] getClientLibCategoriesJsBody() { return this.delegate.getClientLibCategoriesJsBody(); } @Override public String[] getClientLibCategoriesJsHead() { return this.delegate.getClientLibCategoriesJsHead(); } @Override public String getTemplateName() { return this.delegate.getTemplateName(); } @Override public String getAppResourcesPath() { return this.delegate.getAppResourcesPath(); } @Override public NavigationItem getRedirectTarget() { return this.delegate.getRedirectTarget(); } @Override public boolean hasCloudconfigSupport() { return this.delegate.hasCloudconfigSupport(); } @Override public Set<String> getComponentsResourceTypes() { return this.delegate.getComponentsResourceTypes(); } @Override public String[] getExportedItemsOrder() { return this.delegate.getExportedItemsOrder(); } @Override public Map<String, ? extends ComponentExporter> getExportedItems() { return this.delegate.getExportedItems(); } @Override public String getExportedType() { return this.delegate.getExportedType(); } @Override public String getMainContentSelector() { return this.delegate.getMainContentSelector(); } @Override public List<HtmlPageItem> getHtmlPageItems() { return this.delegate.getHtmlPageItems(); } @Override public String getId() { return this.delegate.getId(); } @Override public ComponentData getData() { return this.delegate.getData(); } }
<body>
classes are now present. On top of that, we haven’t broken any out-of-the-box functionality like the page’s JSON model.getData()
, then you have broken the Data Layer integration.Getting Rid of the Boilerplate Code
/** static logger automatically generated. */ @Slf4j @Model(adaptables = { SlingHttpServletRequest.class }, adapters = { Page.class, ContainerExporter.class }, resourceType = "mysite/components/page") @Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION) public final class MyPageModel implements Page, ContainerExporter { /** getter automatically generated. */ @Getter private String cssClassNames; /** delegate the core page model with exclusions. */ @Delegate(excludes = MyDelegateExclusions.class) @Self @Via(type = ResourceSuperType.class) private Page delegate; @PostConstruct public void activate() { log.trace("component activation"); this.cssClassNames = String.join(" ", "lombok", "foo", "bar", this.delegate.getCssClassNames()); } /** signatures excluded from delegation. */ private interface MyDelegateExclusions { /** we don't want this to get delegated. */ String getCssClassNames(); } }
And the code (or lack thereof) pretty much speaks for itself. We got rid of the ubiquitous logger by adding @Slf4J at the class level. Yet our private static log
is still available. We annotated cssClassNames
with @Getter thereby implementing Page.getCssClassNames()
. And of course, we got rid of all the delegated methods by annotating the delegate
field with @Delegate.
What We Learned
For starters, I hope you learned there is a right way to extend core components and models. Second, we learned that Sling models and code generators are not mutually exclusive. Code generators can work in conjunction with Sling Models. You can also leverage them on your OSGi services and any Java class within your application. Take a look at Lombok’s stable features, and then the experimental ones.