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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@ChildResource
ItemTags getTags();
@ChildResource ItemTags getTags();
@ChildResource
ItemTags getTags();

And ItemTags implemented Iterable<Tag>

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
interface ItemTags extends Iterable<Tag>{
}
@Model(adaptables = Resource.class, adapters = ItemTags.class)
finalclass ItemTagsImpl implements ItemTags {
privatefinal List<Tag> tags;
@Inject
publicItemTagsImpl(
@Self@Via("resourceResolver")final TagManager tagManager,
@SelffinalString[] ids){
this.tags = Arrays.stream(ids)
.map(tagManager::resolve)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableList());
}
@Override
public Iterator<Tag>iterator(){
returnthis.tags.iterator();
}
@Override
publicvoidforEach(final Consumer<? super Tag> action){
this.tags.forEach(action);
}
@Override
public Spliterator<Tag>spliterator(){
returnthis.tags.spliterator();
}
}
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(); } }
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@UtilityClass
publicclass TagUtil {
public List<Tag>resolve(final TagManager tagManager, finalString... ids){
return Arrays.stream(ids)
.map(tagManager::resolve)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableList());
}
}
@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()); } }
@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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@ConsumerType
interface Tagged {
List<Tag>getTags();
}
@ConsumerType
interface Item extends Tagged {
StringgetTitle();
Page getPage();
}
@Model(adapters = Tagged.class, adaptables = Resource.class)
class TaggedImpl implements Tagged {
@Getter
private List<Tag> tags;
@ValueMapValue(name = "tags",
injectionStrategy = InjectionStrategy.OPTIONAL)
privateString[] ids;
@Self
@Via("resourceResolver")
private TagManager tagManager;
@PostConstruct
publicvoidactivate(){
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)
finalclass ItemImpl extends TaggedImpl implements Item {
@Getter
@ValueMapValue(name = "jcr:title")
privateString title;
@Getter
@ResourcePath
private Page page;
}
@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; }
@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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
publicinterface TagService {
List<Tag>resolve(TagManager tagManager, String... ids);
}
@Component(service = TagService.class)
publicfinalclass TagServiceImpl implements TagService {
@Override
public List<Tag>resolve(final TagManager tagManager, finalString... ids){
return Arrays.stream(ids)
.map(tagManager::resolve)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableList());
}
}
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()); } }
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@Inject
publicItemTagsImpl(
@Self@Via("resourceResolver")
final TagManager tagManager,
@OSGiServicefinal TagService tagService,
@SelffinalString[] ids){
this.tags = tagService.resolve(tagManager, ids);
}
@Inject public ItemTagsImpl( @Self @Via("resourceResolver") final TagManager tagManager, @OSGiService final TagService tagService, @Self final String[] ids) { this.tags = tagService.resolve(tagManager, ids); }
@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.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// calling the injector by name
@Inject
@Source("valuemap")
privateString name;
// or using the injector-specific annotation
@ValueMapValue
privateString name;
// calling the injector by name @Inject @Source("valuemap") private String name; // or using the injector-specific annotation @ValueMapValue private String name;
// 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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@ServiceRanking(Integer.MAX_VALUE)
@Component(service = { Injector.class})
publicfinalclass TagIdInjector implements Injector {
@Override
publicStringgetName(){
return"tag-id";
}
@Override
public Object getValue(final Object adaptable, finalString 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);
}
}
@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); } }
@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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@Model(adaptables = { Resource.class})
publicinterface MyModel {
@ValueMapValue
StringgetText();
@ChildResource(injectionStrategy = InjectionStrategy.OPTIONAL)
List<Item>getItems();
@Model(adaptables = Resource.class)
interface Item {
@ValueMapValue(name = "jcr:title")
StringgetTitle();
@ResourcePath
Page getPage();
@Inject
@Source("tag-id")
List<Tag>getTags();
}
}
@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(); } }
@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