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.