AEM: @Children Sling model annotation

2019-02-13

I was experimenting with the AEM carousel core component and noticed that the component saves its data in the following way:

I went looking for an easy to use annotation in my Sling model to retrieve all the items as a list of resources for further processing. The closest thing I could find was the following:

List injection for child resources works by injecting grand child resources (since Sling Models Impl 1.0.6). For example, the class

1
2
3
4
5
6
@Model(adaptables=Resource.class)
public class MyModel {

    @Inject
    private List<Resource> addresses;
}

Is suitable for a resource structure such as:

+- resource (being adapted)
|
+- addresses
|
+- address1
|
+- address2
In this case, the addresses List will contain address1 and address2.

As you can see there is no 'items' node as a parent for all items within the carousel so this was of no use.

Solution - Custom @Children Annotation

I wanted to create an easy to use annotation that has the following features:

  • Find direct children based on the sling:resourceType
  • Find direct children based on the node name including regex support

The sling model could look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Children(filterStrategy = ChildrenFilterStrategy.RESOURCE_TYPE, value = "platform-company/components/general/responsive-image")
private List<Resource> childrenByResourceType;

@Children(value = "item_1a")
private List<Resource> childrenByNameImplicit;

@Children(filterStrategy = ChildrenFilterStrategy.NAME, value = "item_2")
private List<Resource> childrenByNameExplicit;

@Children(value = "item_1.*")
private List<Resource> childrenByNameRegex;

@Children(value = "item_1[a-b]")
private List<Resource> childrenByNameRegex2;

Implementation

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

	String value();

	ChildrenFilterStrategy filterStrategy() default ChildrenFilterStrategy.NAME;

	/**
	 * 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;
}
1
2
3
4
public enum ChildrenFilterStrategy {
	NAME,
	RESOURCE_TYPE
}
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
@Component(service = {Injector.class, StaticInjectAnnotationProcessorFactory.class}, property = {
		Constants.SERVICE_RANKING + ":Integer=" + 4301
})
public class ChildrenInjector extends AbstractInjector implements Injector, StaticInjectAnnotationProcessorFactory, AcceptsNullName {

	public static final String NAME = INJECTOR_PREFIX + "children";

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

	@CheckForNull
	@Override
	public Object getValue(@Nonnull Object adaptable, String name, @Nonnull Type type, @Nonnull AnnotatedElement annotatedElement, @Nonnull DisposalCallbackRegistry disposalCallbackRegistry) {


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

		Resource resource = getResource(adaptable);
		FilterStrategyFactory factory = new FilterStrategyFactory();
		FilterStrategy strategy = factory.getFilterStrategy(annotation.filterStrategy());

		if (strategy != null) {
			return strategy.apply(resource, annotation.value());
		} else {
			return null;
		}
	}

	@Override
	public InjectAnnotationProcessor2 createAnnotationProcessor(AnnotatedElement annotatedElement) {

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

	private static class ChildrenAnnotationProcessor extends AbstractInjectAnnotationProcessor2 {

		private final Children annotation;

		ChildrenAnnotationProcessor(final Children annotation) {
			this.annotation = annotation;
		}

		@Override
		public InjectionStrategy getInjectionStrategy() {
			return annotation.injectionStrategy();
		}
	}
}
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
public abstract class AbstractInjector {

	protected static final String INJECTOR_PREFIX = "platform-company-";

	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;
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FilterStrategyFactory {

	public FilterStrategy getFilterStrategy(ChildrenFilterStrategy strategy) {

		if (strategy.equals(ChildrenFilterStrategy.NAME)) {
			return new NameFilterStrategy();
		}

		if (strategy.equals(ChildrenFilterStrategy.RESOURCE_TYPE)) {
			return new ResourceTypeFilterStrategy();
		}

		return null;
	}
}
1
2
3
public interface FilterStrategy {
	List<Resource> apply(Resource parent, String value);
}

sling:resourceType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ResourceTypeFilterStrategy extends AbstractFilterStrategy implements FilterStrategy {

	private String resourceType;

	@Override
	public List<Resource> apply(Resource parent, String value) {
		this.resourceType = value;
		return filterChildren(parent);
	}

	/**
	 * Using contains here to support resource types that have been prefixed (e.g. '/apps/...')
	 */
	@Override
	Predicate<Resource> getFilter() {
		return resource -> resource.getResourceType().contains(resourceType);
	}
}

name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class NameFilterStrategy extends AbstractFilterStrategy implements FilterStrategy {

	private Pattern pattern;

	@Override
	public List<Resource> apply(Resource parent, String value) {
		this.pattern = Pattern.compile(value);
		return filterChildren(parent);
	}

	@Override
	Predicate<Resource> getFilter() {
		return resource -> pattern
				.matcher(resource.getName())
				.matches();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractFilterStrategy {

	public List<Resource> filterChildren(Resource parent) {
		return getChildrenAsStream(parent)
				.filter(getFilter())
				.collect(Collectors.toList());
	}

	private Stream<Resource> getChildrenAsStream(Resource resource) {
		return StreamSupport.stream(resource.getChildren().spliterator(), false);
	}

	abstract Predicate<Resource> getFilter();
}

The code should not be too hard to understand but if you do have any questions, do not hesitate to contact me or leave a comment below.

Created by Jeroen Druwé