I love writing code. What I love more is writing less. Less code means fewer unit tests because you have much less coverage to hit. Well organized, modularized, short, and concise. How do you achieve this? Good software engineering practices, code generators, and leveraging existing frameworks and APIs.
Leveraging Frameworks and APIs
As an AEM developer, you are working within a platform that comes with tons of OOTB frameworks and APIs. Visit the list of OSGi bundles at http://localhost:4502/system/console/bundles. For example, on a recent visit (AEM 6.5.5.0), I noticed Commons BeanUtils 1.8.2. I’ve yet to find a use case for it, but I made a mental note and filed it for a later day.
Sling Models
One of the better-known frameworks is the Sling Models framework. “Better-known” in that we all know the simple use cases. However, few know what is going on under the hood. Enough to merit its own blog post that’s for sure. In the context of this one, this is one of the frameworks to help achieve our goal.
Let’s take the simple use case of the AEM Granite Multifield. It seems every project I’ve worked on has tons of code to read and write multifield data. There are 3 major reasons:
- In the first versions, the multifield could only store simple data. Developers had to extend it and tons of examples existed online. It morphed into the ACS Commons Multifield Extension, now deprecated. And so, some of the code I find today is legacy.
- Developers are not familiar with the injectors and how to leverage them.
- Sometimes developers copy and paste from an old project. Horrible, I know!
Let’s take this cq:dialog example. It has a text and multifield. The multifield is a composite of text, path, and tag fields.
<items jcr:primaryType="nt:unstructured"> <text jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Text" name="./text"/> <multifield jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/multifield" composite="{Boolean}true"> <field jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container" name="./items"> <items jcr:primaryType="nt:unstructured"> <title jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Title" name="./jcr:title"/> <page jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/pathfield" fieldLabel="Page" name="./page"/> <tags jcr:primaryType="nt:unstructured" sling:resourceType="cq/gui/components/coral/common/form/tagfield" fieldLabel="Tags" multiple="{Boolean}true" name="./tags"/> </items> </field> </multifield> </items>
We can write a simple Sling Model interface:
@Model(adaptables = { Resource.class }) public interface MyModel { @ValueMapValue String getText(); @ChildResource(injectionStrategy = InjectionStrategy.OPTIONAL) List<Item> getItems(); @Model(adaptables = Resource.class) interface Item { @ValueMapValue(name = "jcr:title") String getTitle(); @ResourcePath Page getPage(); @ValueMapValue List<String> getTags(); } }
Avoid @Inject. Value injection happens at runtime. If you use @Inject then you are making the framework do extra guesswork. Be explicit. Here are the injector annotations I used along with a link to the source code. It’s worth having a look to better understand how injection works.
- @ValueMapValue – The one you’ll use the most, inject simple resource properties.
- @ChildResource – The data is saved in a resource hierarchy which the framework will further adapt to the Item model.
- @ResourcePath – This will pull up the property value, resolve it to a resource and then adapt it to a Page.
And there you go! The framework provides all the plumbing. No child iteration, DTO classes, resource resolving, or adaptations of any kind. But wait… the tags are really bothering me. I was able to map a page path to an actual Page object. Can’t I map these tag ids to a Tag object?
The short answer is no. Tags are an AEM concept and this is Sling Models. You can’t even resolve a tag id via the resource resolver. You have to use the TagManager. It’s worth noting that there are other open-source injector implementations. Take for example this TagInjector from the ICF Olson AEM Library project.
My Spidey Sense is Tingling
Open-source implementations are fine and everything; but, there are even the injectors from ACS Commons. But my spidey sense is telling me I can still use Sling models and achieve very little code. Challenge accepted! I went to get a cup of coffee and I hung out with my dogs in the backyard for a little while. I did a little thinking about the injection, and this is what I came up with.
The tag ids are saved to a String[] property of the item resource. We know that in Sling, even properties are resources that can be adapted to their base type. For example:
Resource tags = resource.getChild("items/item0/tags"); String[] value = tags.adaptTo(String[].class);
So I changed the return type on getTags() to my own type ItemTags and changed the injector annotation to @ChildResource.
@Model(adaptables = { Resource.class }) public interface MyModel { @ValueMapValue String getText(); @ChildResource(injectionStrategy = InjectionStrategy.OPTIONAL) List<Item> getItems(); @Model(adaptables = Resource.class) interface Item { @ValueMapValue(name = "jcr:title") String getTitle(); @ResourcePath Page getPage(); @ChildResource ItemTags getTags(); } interface ItemTags extends Iterable<Tag> { } @Model(adaptables = Resource.class, adapters = ItemTags.class) final class ItemTagsImpl implements ItemTags { private final List<Tag> tags; @Inject public ItemTagsImpl( @Self @Via("resourceResolver") final TagManager tagManager, @Self final String[] ids) { this.tags = Arrays.stream(ids) .map(tagManager::resolve) .filter(Objects::nonNull) .collect(Collectors.toUnmodifiableList()); } @Override public Iterator<Tag> iterator() { return this.tags.iterator(); } @Override public void forEach(final Consumer<? super Tag> action) { this.tags.forEach(action); } @Override public Spliterator<Tag> spliterator() { return this.tags.spliterator(); } } }
The rest of my decisions were mainly to make unit testing easier.
- I separated the ImageTags into interface and implementation because interfaces make unit testing easier.
- I chose constructor injection because it is simpler to new-up the class in a unit test than setting up mock injections.
- The only code coverage I’m responsible for is now 4 lines of code (the constructor and getter functions)
Furthermore, the object is immutable and iterable for convenience. Note the use of the @Self and @Via annotation in the constructor injection. They tell the framework to take the thing getting adapted, the tags property resource.getChild(“items/item0/tags”), and adapt it to a TagManager by way of the resource resolver. And also adapt it to an array of ids by way of a simple adaptTo(String[].class). Well organized, modularized, short, and concise.
Conclusion: Don’t Forget to Check the Plumbing
It is worth going over the Sling Models documentation and getting to know it. It provides a lot of plumbing. Even if you are forced to implement your own injectors it is a great way to keep your code modular and clean. In the upcoming post, I will cover code generators.
I hope you found this blog post useful. For any additional questions, please comment below and be sure to check out our other Adobe blogs.