AEM: Custom Sling model annotation

2018-02-17

Writing your own custom annotations can increase the readability and re-use of your code. In this short post I'll explain how to create your own annotations that you can use in you Sling models.

The annotation

We will be create an annotation called 'PageTitle' that will retrieve the page title from the properties and append some text to it that was provided within the annotation. There is also a second value that can be passed to the annotation that will control the strategy of injection (optional or not basically).

Code

All code referenced in this post can be found on Github. The repository is based on the official AEM project archetype.

Annotation type definition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@InjectAnnotation
@Source(PageTitleInjector.NAME)
public @interface PageTitle {

    String extra();

    /**
     * if set to REQUIRED injection is mandatory, if set to OPTIONAL injection is optional, in case of DEFAULT
     * the standard annotations ({@link org.apache.sling.models.annotations.Optional}, {@link org.apache.sling.models.annotations.Required}) are used.
     * If even those are not available the default injection strategy defined on the {@link org.apache.sling.models.annotations.Model} applies.
     * Default value = DEFAULT.
     */
    InjectionStrategy injectionStrategy() default InjectionStrategy.DEFAULT;

}

The @ symbol denotes an annotation type definition. That means it is not really an interface, but rather a new annotation type -- to be used as a function modifier, such as @override.

  • We are targeting fields only
  • RetentionPolicy.RUNTIME: Do not discard. The annotation should be available for reflection at runtime. Example: @Deprecated
  • In the definition we specify the data that can be passed into the annotation (you are free to specify what you expect, leaving it completely empty is also an option). The options are extra text or injection strategy (set to optional by default in this example)

Injector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Component(service = {Injector.class, StaticInjectAnnotationProcessorFactory.class}, property = {
        Constants.SERVICE_RANKING + ":Integer=" + 4300
})
public class PageTitleInjector extends AbstractInjector implements Injector, StaticInjectAnnotationProcessorFactory {

    public static final String NAME = "page-title-annotation";

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public Object getValue(final Object adaptable, final String name, final Type type, final AnnotatedElement element,
                           final DisposalCallbackRegistry callbackRegistry) {

        PageTitle annotation = element.getAnnotation(PageTitle.class);
        if (annotation == null) {
            //If the annotation was not found on the element -> return null -> use another injector
            return null;
        }

        // Only class types are supported
        if (!(type instanceof Class<?>)) {
            return null;
        }

        Class<?> requestedClass = (Class<?>) type;

        if (requestedClass.equals(String.class)) {
            Page page = getResourcePage(adaptable);
            return processTitle(page.getTitle(), annotation.extra());
        }

        return null;
    }

    private String processTitle(String title, String extra) {
        return title + " " + extra;
    }

    @Override
    public InjectAnnotationProcessor2 createAnnotationProcessor(AnnotatedElement annotatedElement) {

        PageTitle annotation = annotatedElement.getAnnotation(PageTitle.class);
        if (annotation != null) {
            return new PageTitleAnnotationProcessor(annotation);
        }
        return null;
    }

    private static class PageTitleAnnotationProcessor extends AbstractInjectAnnotationProcessor2 {

        private final PageTitle annotation;

        PageTitleAnnotationProcessor(final PageTitle annotation) {
            this.annotation = annotation;
        }

        @Override
        public InjectionStrategy getInjectionStrategy() {
            return annotation.injectionStrategy();
        }
    }
}
  • Service ranking is important, you don't want conflicts with other injectors, higher ranking injectors get called first.
  • Check within the 'getValue' method if the element is annotated is with your custom annotation. The injector will be called for other annotations as well, we want to return a null in those cases. (returning null -> other injectors will be tested).
  • The 'PageTitleAnnotationProcessor' is used to bind the injection strategy

AbstractInjector

I've created a simple abstract class that can be reused by multiple annotation injectors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class AbstractInjector {

    protected Resource getResource(Object adaptable) {

        if (adaptable instanceof SlingHttpServletRequest) {
            return ((SlingHttpServletRequest) adaptable).getResource();
        }
        if (adaptable instanceof Resource) {
            return (Resource) adaptable;
        }

        return null;
    }

    protected ResourceResolver getResourceResolver(Object adaptable) {

        if (adaptable instanceof SlingHttpServletRequest) {
            return ((SlingHttpServletRequest) adaptable).getResourceResolver();
        }
        if (adaptable instanceof Resource) {
            return ((Resource) adaptable).getResourceResolver();
        }

        return null;
    }

    protected PageManager getPageManager(Object adaptable) {
        ResourceResolver resolver = getResourceResolver(adaptable);

        if (resolver != null) {
            return resolver.adaptTo(PageManager.class);
        }

        return null;
    }


    protected Page getResourcePage(Object adaptable) {

        PageManager pageManager = getPageManager(adaptable);
        Resource resource = getResource(adaptable);

        if (pageManager != null && resource != null) {
            return pageManager.getContainingPage(resource);
        }

        return null;
    }

}

Sling model

1
2
3
4
5
6
7
8
9
10
@Model(adaptables = Resource.class)
public class HelloWorldModel {

    @PageTitle(extra = "Super Awesome Annotation", injectionStrategy = InjectionStrategy.OPTIONAL)
    private String awesomePageTitle;

    public String getAwesomePageTitle() {
        return awesomePageTitle;
    }
}

HTL

1
2
3
<pre data-sly-use.hello="be.jeroendruwe.aem.core.models.HelloWorldModel">
    ${hello.awesomePageTitle}
</pre>

Testing

I created a page with the following page title:

After dragging the component on the page it will display the page title and the extra text ('Super Awesome Annotation') concatenated.

Created by Jeroen Druwé