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);
}
...
There are essentially three parts to a custom injector:
- The annotation which you will use to inject an object into a field (or somewhere else)
- An annotation processor which will feed the attributes of your annotation to the Sling framework
- 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;
}
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;
}
}
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;
}
}
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;
}
}
Deploy this component to your local instance, place it on a page and you should see the following result:
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"
Refresh your page and you should see the value of sessionId
:
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;
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 "";
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;
Finally, we can use the injector in our model:
@CookieValue(cookie = "demo_marketing_consent")
private Boolean marketingConsent;
Deploy and run the following line of code to create the cookie:
document.cookie = "demo_marketing_consent=true"
And now your boolean value is injected too:
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;
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)