Skip to main content

Adobe

Code Reuse With Custom Sling Model Injectors

Reuse@1x.jpg
In my blog Writing Less Java Code in AEM with Sling Models, I talked about writing less code using Sling Models. I followed that up with Writing Less Java Code in AEM with Sling Models & Lombok. I talked about code generators helping in saving time by not writing redundant codes. You could say I am lazy. I like to use the tools and frameworks at my disposal.
 
But sometimes it is inevitable. We have to write code. I am anything if not efficient. When I write code, I like to make it as concise and simple to understand as possible. In this blog, I am going to talk about the reusability of code helping in writing less code.

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

Injectors are at the heart of Sling Models. Take a look at the standard injectors on GitHub. ACS Commons has implemented some injectors. The source code is here. You should take a look at all those implementations. For sure, you will learn something. But before you go from this blog, read on. I will explain a little about how injection works.
 
You have done this a thousand times: 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Juan Ayala

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

Follow Us