Adobe

Writing Less Java Code in AEM with Sling Models & Lombok

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(); }
}
Then we navigate to en.html and we see the <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.
 
There are a couple of downsides to this delegation pattern. First, we have to write monotonous plumbing code to delegate the core model. If code coverage is a rule, we are responsible for 21 lines! Second, it is error-prone. The com.adobe.cq.wcm.core.components.models.Page interface uses default methods. You can omit any one of these from the implementation class. If you omit 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.

About the Author

Juan Ayala is a Lead Developer in the Adobe practice at Perficient, Inc., focused on the Adobe Experience platform and the things revolving around it.

More from this Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up