AEM Guide

Le pattern de délégation Sling : étendre la logique Java des Core Components sans copier-coller

Comment modifier la logique du Sling Model d'un Core Component dans AEM — par exemple la façon dont le Teaser choisit son image — sans forker la classe, grâce à @Via(type = ResourceSuperType.class) de Sling et @Delegate de Lombok.

sling-modelscore-componentsjavalombok

sling:resourceSuperType offre gratuitement l’héritage des scripts HTL : vous étendez core/wcm/components/teaser/v2/teaser, vous surchargez le seul bloc dont vous avez besoin, et Sling remonte la chaîne pour le reste (voir comment Sling résout une requête vers un composant AEM pour le mécanisme complet). Mais sling:resourceSuperType ne change que le script utilisé pour le rendu — il ne touche en rien la classe Java qui se trouve derrière le composant. Si vous devez faire en sorte que le Teaser choisisse une image de repli différente, ou que le composant List trie ses éléments autrement, il faut modifier le Sling Model, et sling:resourceSuperType seul ne suffit pas. C’est exactement à cela que sert le pattern de délégation documenté par Adobe.

Pourquoi on ne peut pas simplement étendre le Sling Model du Core Component

Le réflexe naturel est d’hériter directement de l’implémentation du Core Component — TeaserImpl, ListImpl, etc. Ne le faites pas. Ces classes vivent dans des packages internal (par exemple com.adobe.cq.wcm.core.components.internal.models.v2) qui ne font pas partie de l’API publique exportée par les Core Components. Vous êtes censé coder uniquement contre les interfaces de com.adobe.cq.wcm.core.components.modelsTeaser, List, Image, etc. Même lorsqu’une classe d’implémentation est publique, l’étendre n’est pas un contrat supporté : Adobe peut modifier des constructeurs, la visibilité de champs, ou ajouter final dans une version mineure des Core Components, car l’héritage n’a jamais été le point d’extension prévu.

Le point d’extension supporté est l’interface publique, combinée à la même chaîne sling:resourceSuperType que celle utilisée pour HTL. C’est le pattern de délégation.

Le pattern de délégation : même resource, deux modèles

L’idée : vous écrivez votre propre Sling Model qui implémente la même interface publique que le Core Component (par exemple Teaser), vous l’enregistrez sur le resourceType de votre propre projet — le composant proxy qui déclare déjà sling:resourceSuperType côté HTL — et vous obtenez une instance du modèle original du Core Component pour la même resource sous-jacente en adaptant via le resource super type.

Apache Sling Models dispose d’une annotation conçue exactement pour cela : @Via(type = ResourceSuperType.class) (org.apache.sling.models.annotations.via.ResourceSuperType). Appliquée à un champ injecté avec @Self, elle enveloppe la resource (ou la request) courante en substituant son type de resource par la valeur de sling:resourceSuperType avant d’adapter — donc au lieu d’adapter à nouveau la resource courante (ce qui provoquerait une récursion vers votre propre modèle), elle adapte une copie typée comme le Core Component, ce qui résout vers le Sling Model propre du Core Component :

<!-- /apps/myproject/components/teaser/.content.xml -->
<jcr:root
    jcr:primaryType="cq:Component"
    jcr:title="Teaser"
    sling:resourceSuperType="core/wcm/components/teaser/v2/teaser"/>
@Self
@Via(type = ResourceSuperType.class)
private Teaser coreTeaser;

À l’exécution, coreTeaser est exactement la même instance de TeaserImpl (ou la classe interne utilisée par le Core Component) que celle qui aurait été adaptée si votre composant proxy n’existait pas — vous ne réimplémentez pas sa logique, vous l’enveloppez.

Éviter le boilerplate avec @Delegate de Lombok

Teaser étend Component et déclare une bonne dizaine de méthodes (getTitle(), getPretitle(), getLink(), getImageResource(), isActionsEnabled(), getActions(), et bien d’autres, en plus de tout ce qui est hérité de Component). Écrire à la main une méthode de redirection pour chacune d’elles juste pour en modifier une seule, c’est exactement le copier-coller que ce pattern vise à éviter.

C’est là qu’intervient @Delegate de Lombok (lombok.experimental.Delegate — elle se trouve dans le package expérimental de Lombok, mais c’est l’outil standard utilisé par la communauté AEM pour cela) : placée sur le champ coreTeaser, elle génère à la compilation une méthode de redirection pour chaque méthode publique de Teaser. Pour garder une méthode pour vous, vous l’excluez avec @Delegate(excludes = ...), en pointant vers une petite interface marqueur qui ne déclare que la ou les signatures que vous surchargez — sinon, la méthode de redirection générée par Lombok et votre propre @Override entrent en collision avec une erreur de compilation « méthode dupliquée ».

Lombok lui-même est ajouté comme dépendance en scope provided sur le bundle core — il n’est nécessaire qu’à la compilation, pour le traitement des annotations, pas à l’exécution.

Un exemple complet : surcharger la façon dont le Teaser choisit son image

Teaser.getImageResource() (ajoutée dans Core Components 12.4.0) peut retourner null lorsque le teaser n’a pas d’image propre et qu’aucun de ses contenus liés n’en fournit une. Supposons que le design system du projet impose qu’un teaser ne soit jamais rendu sans image — dans ce cas on utilise un asset de repli partagé :

package com.myproject.core.models;

import com.adobe.cq.wcm.core.components.models.Teaser;
import lombok.experimental.Delegate;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.via.ResourceSuperType;

@Model(adaptables = SlingHttpServletRequest.class,
       adapters = Teaser.class,
       resourceType = MyTeaser.RESOURCE_TYPE)
public class MyTeaser implements Teaser {

    static final String RESOURCE_TYPE = "myproject/components/teaser";

    private static final String FALLBACK_IMAGE_PATH =
            "/content/dam/myproject/defaults/teaser-fallback.png";

    @Self
    private SlingHttpServletRequest request;

    @Self
    @Via(type = ResourceSuperType.class)
    @Delegate(excludes = Overrides.class)
    private Teaser coreTeaser;

    /**
     * Les méthodes listées ici sont exclues de la redirection générée
     * par Lombok afin de pouvoir les surcharger ci-dessous sans erreur
     * de compilation « méthode dupliquée ».
     */
    private interface Overrides {
        Resource getImageResource();
    }

    @Override
    public Resource getImageResource() {
        Resource image = coreTeaser.getImageResource();
        return image != null
                ? image
                : request.getResourceResolver().getResource(FALLBACK_IMAGE_PATH);
    }
}

Toutes les autres méthodes de MyTeasergetTitle(), getLink(), isActionsEnabled(), getExportedType() héritée de Component, tout — sont générées par Lombok et redirigent simplement vers coreTeaser. Vous n’avez écrit que la seule méthode que vous deviez réellement modifier.

Pourquoi le script HTL n’a pas besoin de changer

Comme le script HTL du composant proxy est lui-même hérité sans modification du Core Component (c’est le mécanisme sling:resourceSuperType côté HTL), il contient toujours quelque chose comme data-sly-use.teaser="com.adobe.cq.wcm.core.components.models.Teaser" — il adapte vers l’interface, pas vers une classe concrète. Sling Models détermine quelle implémentation @Model enregistrée utiliser pour cette interface en fonction du sling:resourceType réel de la resource, en préférant la correspondance la plus proche lorsque plusieurs modèles déclarent la même interface d’adaptation. Pour une resource dont le sling:resourceType est myproject/components/teaser, c’est MyTeaser — le script HTL hérité se met donc à afficher votre logique d’image de repli sans qu’une seule ligne de markup ne change.

Erreurs courantes sur de vrais projets AEM