sling models custom injector header image

Sling Models: How to write Sling Model Injector

Once I needed to write several debugging servlets for some functionality. Those servlets had to accept a big pile of parameters and I was tired of having tens of rows like  String paramValue = request.getParameter(“paramName”). Luckily, I’ve already known Sling Models. So I’ve decided to find the way to inject these parameters into the model, so I could write just request.adaptTo(MyModel.class) . Unfortunately, I found nothing, so decided to write it myself. So in this article, I will show you how to write custom Sling Model injector on the example of Sling Model Request Parameter Injector.

Custom Sling Model Injector

To write it, we need to implement an OSGi service, which inherits from Injector interface.  It will force us to implement 2 methods:

  • getName – returns string, which will be used as logical name for the injector.
  • getValue – returns a value for an injection point.

The last one has several parameters and we will review some of them a bit later.

Sling Model Request Parameter Injector

Let’s imagine, that we have model RequestParamsInjected where we have several fields, which should be populated with request parameters.

package com.taradevko.aem.model;

import com.taradevko.aem.model.injector.annotation.RequestParameter;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Model;

import java.util.HashMap;
import java.util.Map;

@Model(adaptables = SlingHttpServletRequest.class)
public class RequestParamsInjected {
    private static final Map<Object, String> CONTENT = new HashMap<>();
    static {
        CONTENT.put("param1", "Content 1");
        CONTENT.put(963, "Content 2");
    }

    private String stringParam;

    private Integer integerParam;

    public String getStringContent() {
        return CONTENT.get(stringParam);
    }

    public String getIntegerContent() {
        return CONTENT.get(integerParam);
    }
}

For injecting values into the model fields, we need to annotate them. Actually, we can use existing annotations @Inject and @Source but it’s not flexible at all. We will write our own annotation!

package com.taradevko.aem.model.injector.annotation;

import org.apache.sling.models.annotations.Source;
import org.apache.sling.models.spi.injectorspecific.InjectAnnotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@InjectAnnotation
@Source("request-parameter")
public @interface RequestParameter {
}

We mark an annotation to be used with fields only, however you can have it for methods and parameters as well if you specify them in @Target. Then we declare our annotation as custom injection annotation. And finally, we specify which logical name to be used with for annotation.

Now we can mark fields with new annotations:

package com.taradevko.aem.model;

...

@Model(adaptables = SlingHttpServletRequest.class)
public class RequestParamsInjected {

    ....

    @RequestParameter
    private String stringParam;

    @RequestParameter
    private Integer integerParam;

    ...
}

Without injector our annotation is useless. Let’s fix it:

package com.taradevko.aem.model.injector;

import com.taradevko.aem.model.injector.annotation.RequestParameter;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.models.spi.DisposalCallbackRegistry;
import org.apache.sling.models.spi.Injector;
import org.osgi.framework.Constants;

import javax.servlet.ServletRequest;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;

@Component
@Service
@Property(name = Constants.SERVICE_RANKING, intValue = Integer.MIN_VALUE)
public class RequestParameterInjector implements Injector {
    @Override
    public String getName() {
        return "request-parameter";
    }

    @Override
    public Object getValue(final Object adaptable,
                           final String fieldName,
                           final Type type,
                           final AnnotatedElement annotatedElement,
                           final DisposalCallbackRegistry disposalCallbackRegistry) {

        if (adaptable instanceof ServletRequest) {
            final ServletRequest request = (ServletRequest) adaptable;
            if (type instanceof Class<?>) {
                Class<?> fieldClass = (Class<?>) type;
                return getValue(request, fieldClass, fieldName);
            }
        }
        return null;
    }

    private Object getValue(final ServletRequest request, final Class<?> fieldClass, final String fieldName) {
        String parameterValue = request.getParameter(fieldName);
        if (StringUtils.isBlank(parameterValue)) {
            return null;
        }

        if (fieldClass.equals(Integer.class)) {
            try {
                return Integer.parseInt(parameterValue);
            } catch (NumberFormatException ex) {

                //got exception, so not an integer, return null;
                return null;
            }
        }

        return parameterValue;
    }
}

Earlier we discussed briefly methods we override above. Now it’s time to go through the method getValue:

  • for now we will use first 3 arguments:
    • adaptable – object which Sling tries to adapt from. We expect it to be the request;
    • fieldName – name of the field. which has been annotated;
    • type – the declared type of the injection point;
  • first, we check if adaptable is request and type is Class;
  • then we call method getValue where we get parameter by field name and cast it to the integer if injection field is of type Integer;
  • finally, we return integer, string or null value.

For testing, let’s create simple component:

<pre data-sly-use.model="com.taradevko.aem.model.RequestParamsInjected">
    Your parameter brought next content:
    ${model.stringContent}
    ${model.integerContent}
</pre>

We are ready to test it:

sling model request parameter inject first example

As you can see, both parameters were injected into the fields successfully. But what if one of the parameters is not specified? Our model will not be adapted because both fields are required. Other annotations, like OSGiService, has nice attribute optional which can be used in such cases. Let’s add such feature for our annotation:

package com.taradevko.aem.model.injector.annotation;

...

public @interface RequestParameter {
 boolean optional() default false;
}

And now adapt model as well:

package com.taradevko.aem.model;

...

@Model(adaptables = SlingHttpServletRequest.class)
public class RequestParamsInjected {
...
    @RequestParameter(optional = true)
    private Integer integerParam;
...
}

Why not test it now?

sling model inject optional not work

Oops, does not work. But why?

Custom Inject Annotation Processor

It appears, that to make custom annotation fully working, we need to implement one more interface – InjectAnnotationProcessorFactory (note that in latest versions this interface is marked as deprecated and StaticInjectAnnotationProcessorFactory should be used instead).

Let’s implement it.

package com.taradevko.aem.model.injector;

...

@Component
@Service
@Property(name = Constants.SERVICE_RANKING, intValue = Integer.MIN_VALUE)
public class RequestParameterInjector implements Injector, InjectAnnotationProcessorFactory {
    
    ...

   
    @Override
    public InjectAnnotationProcessor createAnnotationProcessor(final Object adaptable, final AnnotatedElement element) {
        
        // check if the element has the expected annotation
        RequestParameter annotation = element.getAnnotation(RequestParameter.class);
        if (annotation != null) {
            return new RequestParameterAnnotationProcessor(annotation);
        }
        return null;
    }

    private static class RequestParameterAnnotationProcessor extends AbstractInjectAnnotationProcessor {

        private final RequestParameter annotation;

        RequestParameterAnnotationProcessor(RequestParameter annotation) {
            this.annotation = annotation;
        }

        @Override
        public Boolean isOptional() {
            return annotation.optional();
        }
    }
}

There, we create annotation processor, which overrides isOptional and returns corresponding value from the annotation. After this changes, it works:

sling model request parameter optional

Custom Annotation Attribute

Let’s imagine that now we have new requirement – we need to get parameter from the request, but we do not know its name. However, we have a regular expression which we can use to find it.

First, we need to update our annotation – add regexp attribute to it:

package com.taradevko.aem.model.injector.annotation;

...

public @interface RequestParameter {
    boolean optional() default false;
    String regexp() default "";
}

Then we need to update injector:

package com.taradevko.aem.model.injector;

...

public class RequestParameterInjector implements Injector, InjectAnnotationProcessorFactory {

    ...

    @Override
    public Object getValue(final Object adaptable,
                           final String fieldName,
                           final Type type,
                           final AnnotatedElement annotatedElement,
                           final DisposalCallbackRegistry disposalCallbackRegistry) {

        if (adaptable instanceof ServletRequest) {
            final ServletRequest request = (ServletRequest) adaptable;
            String parameterName = null;

            final RequestParameter annotation = annotatedElement.getAnnotation(RequestParameter.class);
            if (annotation != null && StringUtils.isNotBlank(annotation.regexp())) {
                parameterName = findParameterName(request, annotation.regexp());
            }

            if (type instanceof Class<?>) {
                Class<?> fieldClass = (Class<?>) type;
                return getValue(request, fieldClass, StringUtils.defaultString(parameterName, fieldName));
            }
        }
        return null;
    }

    private String findParameterName(final ServletRequest request, final String paramNameRegexp) {
        final Enumeration parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            final String parameterName = (String) parameterNames.nextElement();

            if (parameterName.matches(paramNameRegexp)) {
                return parameterName;
            }
        }

        return null;
    }

    ...
}

There, before we can get value, we need to find parameter’s name if regular expression is provided. To do this, we iterate over all parameters and match them with regexp from our annotation.

And add a new field with specified regular expression:

package com.taradevko.aem.model;

...

@Model(adaptables = SlingHttpServletRequest.class)
public class RequestParamsInjected {
    private static final Map<Object, String> CONTENT = new HashMap<>();
    static {
        CONTENT.put("param1", "Content 1");
        CONTENT.put(963, "Content 2");
        CONTENT.put("regexpparam", "Content for regexp");
    }

    ...

    @RequestParameter(regexp = "\\d\\w{2}\\d", optional = true)
    private String regexpParam;

    ...

    public String getRegexpContent() {
        return CONTENT.get(regexpParam);
    }
}

Finally, let’s adapt our test component:

<pre data-sly-use.model="com.taradevko.aem.model.RequestParamsInjected">
    Your parameter brought next content:
    string: ${model.stringContent}
    integer: ${model.integerContent}
    regexp: ${model.regexpContent}
</pre>

Test it? Test it!

sling models request parameter regular expression

Great, it works as expected.

Conclusion:

Even though we’ve implemented pretty basic Request Parameter injector, it can give you the insight of how to write more advanced version. Being able to write custom injectors gives nice tool for making your logic cleaner and more maintainable.

 

P.S.: will be happy to get feedback and questions from you and see you soon.

You can find the complete code in this GitHub repo.

5 thoughts on “Sling Models: How to write Sling Model Injector

  1. I am having an issue, all other controllers for example a class that has a field injected with @Inject also call the getValue of my new annotation. It seems that the @Source does not work. I am using aem 6.3.1.2. Any idea?

    1. JDRUWE, when you use @Inject, Sling does not know, which Injector should be used to inject the value, so it goes through all the Injectors and stops as soon as first non-null value is obtained.

Your thoughts are welcome