Code generator in AEM with Sling Models & Lombok
Have you ever though about using code generators in AEM. Code generators save us from having to write monotonous Java plumbing code like bean?getters andsetters, loggers,?equals(),?toString()?
and?hashCode() which get repeated. It would be lot better if we can focus more on high-level business logic instead of writing and maintaining this plumbing code.
Next question is how we can achieve this – There are quite a few tools which serves the purpose, but I think Lombok is the most preferred one. I will not go into the details of it, but to summarize it generate value objects. But apart from that it does so much more than that.
We can add “Lombok” in our maven project as maven dependency. To include lombok as a 'provided' dependency, add it to your?<dependencies>?block like so:
<dependencies
???????? <dependency>
????????????????? <groupId>org.projectlombok</groupId>
????????????????? <artifactId>lombok</artifactId>
????????????????? <version>1.18.22</version>
????????????????? <scope>provided</scope>
???????? </dependency>
</dependencies>>
After this dependency added, all your Lombok annotated classes will get augmented with generated code. Well. 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 more Lombok?here.
Setting it up with IDE is also easy, you will lot of details online. the latest version of IntelliJ is already?compatible?with Lombok. Eclipse requires some setup.
Let’s try to understand with an example:
A Practical Example
As an AEM developer you must have added a class to <body> tag. What would be the proper way of extending WCM Core Page Template Component. To extend WCM Core Page Template, the predictable reaction would be to overwrite the?page.html?file where the?<body>?tag is. But this is not the right ways of extending it, because that file has lot of things in terms of support Adobe’s platform & features. By overwrite, we would have to take care that we keep it up to date between upgrades and this is not a east thing to do with so many probability.
So what is the proper way of doing it, The proper way is to extend the core model through the?ResourceSuperType, a?Via provider type.
Let’s 1st create a project using the new archetype:
领英推荐
mvn -B archetype:generate
-D archetypeGroupId=com.adobe.aem \
-D archetypeArtifactId=aem-project-archetype \
-D archetypeVersion=26 \
-D appTitle="My TestSite" \
-D appId="mytestsite" \
-D groupId="com.mytestsite" \
-D aemVersion=6.5.5\
Note: I am using AEM 6.5 and Archetype 26.
And the first class we will create is?com.mytestsite.core.models.MyTestPageModel
@Model(adaptables = { SlingHttpServletRequest.class }, adapters = { Page.class, ContainerExporter.class }, resourceType = "mytestsite/components/page"
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public final class MyTestPageModel implements Page, ContainerExporter {
private static final Logger log = LoggerFactory.getLogger(MyTestPageModel.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();
?}
}
Now if you navigate to “en.html”, you will see that the <body> classes are present without breaking any out-of-the-box functionality like the page’s?JSON model.
Everything is fine till now, But if you observe the code closely, you will see couple of downsides to this delegation pattern
1st the monotonous plumbing code to delegate the core model and if you consider the code coverage rule, you will observe that we have more than 20 lines of code.
2nd, This code is error-prone?and will lot of extra effort to maintain it.
So what option do we have to avoid this. 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.
Avoid the Boilerplate Code
/** static logger automatically generated. *
@Slf4j
@Model(adaptables = { SlingHttpServletRequest.class },
adapters = { Page.class, ContainerExporter.class },
resourceType = "mytestsite/components/page")
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public final class MyTestPageModel 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();
}
}/
In the above sample code, We got ride of the abundant logger by adding?@Slf4J?at the class level. But still our private static?log?is still available. We have annotated?cssClassNames?with?@Getter?thereby implementing?Page.getCssClassNames().?Apart from that we got rid of all the delegated methods by annotating the?delegate?field with?@Delegate.
Conclusion
You can see that there is a way(right) to extent core component and models. Second, Sling models and code generators are not mutually exclusive. i.e. both of then can take place at same time. It’s not like they cannot happen together. Code generators can work in combination with Sling Models and should be part of your coding good practice. Lombok can also be leverage on your OSGi services and any Java class within your application.