Tag ID Model
As an example, I am going to revisit the Tag id model from that first blog post. In that example, the multifield items had a tag ids property named ./tags
. This I injected into the model as ItemTags
@ChildResource ItemTags getTags();
And ItemTags
implemented Iterable<Tag>
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 Sling Model code has the smallest amount of code possible. Yet, tags are ubiquitous in AEM. The chances that I may need to put in place another model that also needs to resolve tags are high. Let us explore some of the code reuse options we have.
A Utility Class
The knee-jerk reaction would probably be to create a class along the lines of TagUtil.java
. With Lombok’s @UtilityClass, it is very easy
@UtilityClass public class TagUtil { public List<Tag> resolve(final TagManager tagManager, final String... ids) { return Arrays.stream(ids) .map(tagManager::resolve) .filter(Objects::nonNull) .collect(Collectors.toUnmodifiableList()); } }
Read the @UtilityClass documentation. A utility class is final, its functions static, and may not be instantiated. Thus, it is difficult to mock in unit tests. To isolate the code you want to test, you would need PowerMockito. This is bad. If you are having to resort to PowerMockito, then you are not writing testable code. There are plenty of other reasons why you should avoid utility classes. I avoid them like the plague.
An Inherited Base Class
I grew up in an object-oriented world. My second idea might be an object-oriented approach. And inheritance works with Sling Models.
@ConsumerType interface Tagged { List<Tag> getTags(); } @ConsumerType interface Item extends Tagged { String getTitle(); Page getPage(); } @Model(adapters = Tagged.class, adaptables = Resource.class) class TaggedImpl implements Tagged { @Getter private List<Tag> tags; @ValueMapValue(name = "tags", injectionStrategy = InjectionStrategy.OPTIONAL) private String[] ids; @Self @Via("resourceResolver") private TagManager tagManager; @PostConstruct public void activate() { this.tags = Arrays.stream(ArrayUtils.nullToEmpty(this.ids)) .map(this.tagManager::resolve) .filter(Objects::nonNull) .collect(Collectors.toUnmodifiableList()); } } @Model(adapters = Item.class, adaptables = Resource.class) final class ItemImpl extends TaggedImpl implements Item { @Getter @ValueMapValue(name = "jcr:title") private String title; @Getter @ResourcePath private Page page; }
In the example above, I have abandoned the interface-only approach in favor of a class because I had to extend TaggedImpl
. OOP might make sense in certain scenarios where you have a set of related objects. But in this case, the concept of “tagged” is very fluid and can apply to almost anything, like a page, a component, or an asset. What if you were to add another concept like “linked,” as in a component could have a link? Then Item would extend Tagged
which would extend Linked
.
An OSGi Component
I may have grown up in an object-oriented world, but I’ve been stomping around an OSGi container for the past few years. My third idea would be an OSGi component.
public interface TagService { List<Tag> resolve(TagManager tagManager, String... ids); } @Component(service = TagService.class) public final class TagServiceImpl implements TagService { @Override public List<Tag> resolve(final TagManager tagManager, final String... ids) { return Arrays.stream(ids) .map(tagManager::resolve) .filter(Objects::nonNull) .collect(Collectors.toUnmodifiableList()); } }
You can separate the interface from the implementation making it mockable. This has one annoying shortcoming. It is not integrated into the Sling Model’s injection pipeline. I still need to write custom code to invoke that resolve(TagManager, String...)
function, within the model’s constructor
@Inject public ItemTagsImpl( @Self @Via("resourceResolver") final TagManager tagManager, @OSGiService final TagService tagService, @Self final String[] ids) { this.tags = tagService.resolve(tagManager, ids); }
A Custom Injector
MyModel model = resource.adaptTo(MyModel.class)
. Did you ever wonder why this works? How do all those properties in the model get injected? Take a look at the ModelAdapterFactory.createObject(Object, ModelClass<ModelType>) Injection happens by rolling over all the injectors until one returns a value. You need to be more explicit. Either call the injector by name or use the injector-specific annotation.// calling the injector by name @Inject @Source("valuemap") private String name; // or using the injector-specific annotation @ValueMapValue private String name;
So let’s create our custom injector. If you liked the OSGi component solution above, this is not that different. An injector is simply an OSGi component that is used by the ModelAdapterFactory.
@ServiceRanking(Integer.MAX_VALUE) @Component(service = { Injector.class }) public final class TagIdInjector implements Injector { @Override public String getName() { return "tag-id"; } @Override public Object getValue(final Object adaptable, final String name, final Type type, final AnnotatedElement element, final DisposalCallbackRegistry callbackRegistry) { final var tags = new ArrayList<Tag>(); final var resource = (Resource) adaptable; final var ids = resource.getValueMap() .get(name, String[].class); if (ids != null) { Optional.ofNullable(resource.getResourceResolver() .adaptTo(TagManager.class)) .ifPresent(tagManager -> Arrays.stream(ids) .map(tagManager::resolve) .filter(Objects::nonNull) .forEach(tags::add)); } return Collections.unmodifiableList(tags); } }
Of course, this is a very simple example. The getValue
function is making some assumptions. It assumes the adaptable is a Resource
. And it will always return a non-null List<Tag>
. This is fine because our model is adapting a resource and we will be calling it by its name: tag-id
.
Below is the final model code.
@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(); @Inject @Source("tag-id") List<Tag> getTags(); } }
That is a heck of a lot smaller than the original code. I am back to interfaces which means I do not need to cover this code with unit tests. The only thing I need to cover is the injector.
Conclusion
If you looked at ModelAdapterFactory.createObject(Object, ModelClass<ModelType>) code, now you know. @Inject
is not magic. Utility classes are bad. Custom injectors are your best choice when it comes to creating reusable injectors.
We used the injector name @Source("tag-id")
. I would leave it up to you as an exercise to put in place your own custom annotation. Take a look at the open-source injector implementations. And keep an eye out for the StaticInjectAnnotationProcessorFactory
interface.