DEV Community

Cover image for Custom Sling Injector With Annotations
Theo Pendle
Theo Pendle

Posted on • Edited on

Custom Sling Injector With Annotations

How to inject anything anywhere with Sling

In this article I will show you how to build a custom injector for your Sling models with a real use-case. The source code is available on Github if you scroll to the bottom of the article.

The use case: hungry for cookies

One common use case I've seen is the need to read cookies in order to determine some behaviour in a set of components. Let's have a look at how we can use injection to write a re-usable solution to this requirement.

The objective will be to achieve something like this:

    ...

    // Read the cookie value here
    @CookieValue
    private String sessionId;

    @PostConstruct
    protected void init() {

        // Use the cookie value here
        log.info("Session ID <{}>", sessionId);
    }

    ...
Enter fullscreen mode Exit fullscreen mode

There are essentially three parts to a custom injector:

  1. The annotation which you will use to inject an object into a field (or somewhere else)
  2. An annotation processor which will feed the attributes of your annotation to the Sling framework
  3. The injector itself, which is responsible for providing the object

Let's start by creating our annotation. I will give code examples with an extra-generous serving of comments to explain each line ๐Ÿ‘

Creating the annotation

To create your annotation, write the following Java @interface:


// This annotation belongs on a method (eg: getter), field or parameter (eg: constructor parameter)
@Target({ElementType.FIELD, ElementType.PARAMETER})

// Retained at runtime, as opposed to the Lombok @Getter which is discarded for example
@Retention(RetentionPolicy.RUNTIME)

// Declares an annotation as a custom inject annotation.
@InjectAnnotation

// This string will tell Sling which injector to use for this value
@Source("cookie-value")
public @interface CookieValue {

    // Default to OPTIONAL injection strategy as we cannot rely on the cookie being present
    InjectionStrategy injectionStrategy() default InjectionStrategy.OPTIONAL;
}

Enter fullscreen mode Exit fullscreen mode

Processing the annotation

Now that we have an annotation to indicate that a value should by read from a cookie, we must tell the Sling framework how to interpret it.

The most important piece of information for Sling to know is the injectionStrategy should, by default be OPTIONAL. If not, then any Sling model that attempts to read a cookie and fails, would throw an exception.

To create our annotation processor, we must implement the InjectAnnotationProcessor2 interface.


@AllArgsConstructor
public class CookieValueProcessor implements InjectAnnotationProcessor2 {

    private CookieValue annotation;

    // This is the only we wish to take from the CookieValue annotation
    @Override
    public InjectionStrategy getInjectionStrategy() {
        return annotation.injectionStrategy();
    }

    // We can leave all these as default
    @Override
    public String getName() {
        return null;
    }

    @Override
    public String getVia() {
        return null;
    }

    @Override
    public boolean hasDefault() {
        return false;
    }

    @Override
    public Object getDefault() {
        return null;
    }

    @Override
    public Boolean isOptional() {
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

Injecting the cookie value

Finally we are ready to write the logic to read a value from a cookie. To do that, we must write the injector. This class will also server to bind the annotation to the processor from the previous steps:


/**
 * Injects the value of a cookie derived from a SlingHttpServletRequest.
 */
@Slf4j
@Component(service = {Injector.class, StaticInjectAnnotationProcessorFactory.class})
public class CookieValueInjector implements Injector, StaticInjectAnnotationProcessorFactory {

    @Override
    public String getName() {
        return "cookie-value";
    }

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

        // Injectors are cycled through, so it is important to perform this check.
        // We only want to trigger our code if the annotation being processed is indeed CookieValue
        final CookieValue annotation = element.getAnnotation(CookieValue.class);
        if (annotation == null) {
            return null;
        }

        // We cannot get a cookie from a resource adaptable, so let's make sure we indeed have a request to read from
        if (!(adaptable instanceof SlingHttpServletRequest)) {
            log.error("Cannot adapt <{}> to cookie value", adaptable.getClass().getSimpleName());
            return null;
        }

        final SlingHttpServletRequest request = (SlingHttpServletRequest) adaptable;

        final Cookie cookie = request.getCookie(name);
        if (cookie == null) {
            log.debug("Could not read value from cookie <{}> as no such cookie exists", name);
            return null;
        }

        return cookie.getValue();
    }

    // This rather clunky method in conjunction with CookieValueProcessor to pass the values of the annotation to the
    // Sling framework. Especially critical for the InjectionStrategy!
    @Override
    public InjectAnnotationProcessor2 createAnnotationProcessor(final AnnotatedElement element) {
        // Check if the element has the expected annotation
        final CookieValue annotation = element.getAnnotation(CookieValue.class);
        if (annotation != null) {
            return new CookieValueProcessor(annotation);
        }
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

And there you have it! Now lets use what we've just created in a Sling model.

Using our injector

In order to test our work so far, I'll inject a cookie value into a very basic Sling Model:


@Model(adaptables = SlingHttpServletRequest.class)
public class HelloWorldImpl implements HelloWorld {

    // Here we use our injector to tell Sling to read the value of the "sessionId" cookie.
    @CookieValue
    private String sessionId;

    @Getter
    private String message;

    @PostConstruct
    protected void init() {
        message = "Session ID: " + sessionId;
    }
}
Enter fullscreen mode Exit fullscreen mode

Deploy this component to your local instance, place it on a page and you should see the following result:

Session ID is null

No worries! The cookie doesn't exist yet. Let's create it by running the following code in the console of your browser:

document.cookie = "sessionId=123456789"
Enter fullscreen mode Exit fullscreen mode

Refresh your page and you should see the value of sessionId:

Session id is correct

And that's how you write a custom injector! ๐Ÿ’‰

Kicking it up a notch

But what if we want to de-couple the field name from the cookie name? And what if we want to inject not a String but rather a boolean value, such as a marketing consent flag?

Well stick around to find out.

Injecting a custom type

Changing the type of the injected value is relatively easy. Since the getValue() method of the Injector interface returns an Object, Sling basically casts the return value to the type of the field into which the injection happens.

However, it is our job to find out which type should be returned. So let's add this piece of logic before to our injector class:

// Check which type we should return, and convert accordingly
final String stringValue = cookie.getValue();
if (type.equals(String.class)) {
    return stringValue;
}
if (type.equals(Boolean.class)) {
    return Boolean.parseBoolean(stringValue);
}

// If the type of the field is not something we expect, then return null
log.error("Cookie value can only be injected as type <{}>", type.getClass());
return null;
Enter fullscreen mode Exit fullscreen mode

Reading a custom cookie name

To make it possible to specify the name of the cookie to read from, we will have to give our annotation another attribute. Add this method to the CookieValue annotation:

/**
 * The name of the cooke to read the value from
 */
String cookie() default "";
Enter fullscreen mode Exit fullscreen mode

Then add the following logic to the injector:

// Use the cookie name if provided, else use the name of the annotated element (eg: field name)
final String cookieName = StringUtils.isNotBlank(annotation.cookie())
        ? annotation.cookie()
        : name;
Enter fullscreen mode Exit fullscreen mode

Finally, we can use the injector in our model:

@CookieValue(cookie = "demo_marketing_consent")
private Boolean marketingConsent;
Enter fullscreen mode Exit fullscreen mode

Deploy and run the following line of code to create the cookie:

document.cookie = "demo_marketing_consent=true"
Enter fullscreen mode Exit fullscreen mode

And now your boolean value is injected too:

Marketing consent cookie is true

More examples

The source code for this tutorial is on GitHub.

I've created a diff here so you can see exactly what changes to make. It includes another example of a custom annotation that lets you do this:


// Inject the tags associated to the page on which this component is placed
@PageTag
private List<Tag> tags;

// Inject a single tag from a custom page property
@PageTag(name = "customTag")
private Tag customTag;
Enter fullscreen mode Exit fullscreen mode

Donโ€™t hesitate to contact me on LinkedIn if you have any questions/ideas or if you just want to discuss all things Sling or AEM! ๐Ÿ‘

Top comments (0)