AEM Guide

Il pattern di delegation di Sling: estendere la logica Java dei Core Component senza copia-incolla

Come modificare la logica del Sling Model di un Core Component in AEM — ad esempio il modo in cui il Teaser sceglie la propria immagine — senza forkare la classe, usando @Via(type = ResourceSuperType.class) di Sling e @Delegate di Lombok.

sling-modelscore-componentsjavalombok

sling:resourceSuperType regala l’ereditarietà degli script HTL: estendi core/wcm/components/teaser/v2/teaser, sovrascrivi l’unico blocco di cui hai bisogno e Sling risale la catena per il resto (vedi come Sling risolve una richiesta verso un componente AEM per il meccanismo completo). Ma sling:resourceSuperType cambia solo quale script esegue il rendering — non tocca affatto la classe Java dietro al componente. Se ti serve che il Teaser scelga un’immagine di fallback diversa, o che il componente List ordini gli elementi in un altro modo, devi mettere mano al Sling Model, e sling:resourceSuperType da solo non ti porta fin lì. È esattamente per questo che esiste il pattern di delegation documentato da Adobe.

Perché non puoi semplicemente estendere il Sling Model del Core Component

L’istinto è ereditare direttamente dall’implementazione del Core Component — TeaserImpl, ListImpl e così via. Non farlo. Quelle classi vivono in package internal (ad esempio com.adobe.cq.wcm.core.components.internal.models.v2) che non fanno parte dell’API pubblica esportata dai Core Component. Sei tenuto a programmare solo contro le interfacce di com.adobe.cq.wcm.core.components.modelsTeaser, List, Image e così via. Anche quando una classe di implementazione risulta pubblica, estenderla non è un contratto supportato: Adobe può cambiare costruttori, visibilità dei campi o aggiungere final in una versione minore dei Core Component, perché l’ereditarietà non è mai stata il punto di estensione previsto.

Il punto di estensione supportato è l’interfaccia pubblica, combinata con la stessa catena di sling:resourceSuperType usata per HTL. Questo è il pattern di delegation.

Il pattern di delegation: stessa resource, due model

L’idea: scrivi il tuo Sling Model che implementa la stessa interfaccia pubblica del Core Component (ad esempio Teaser), lo registri sul resourceType del tuo progetto — il componente proxy che dichiara già sling:resourceSuperType lato HTL — e ottieni un’istanza del model originale del Core Component per la stessa resource sottostante adattando tramite il resource super type.

Apache Sling Models ha un’annotazione pensata esattamente per questo: @Via(type = ResourceSuperType.class) (org.apache.sling.models.annotations.via.ResourceSuperType). Applicata a un campo iniettato con @Self, avvolge la resource (o la request) corrente sostituendone il resource type con il valore di sling:resourceSuperType prima di adattare — quindi invece di adattare di nuovo la resource corrente (il che causerebbe una ricorsione verso il tuo stesso model), adatta una copia tipizzata come il Core Component, che si risolve nel Sling Model proprio del 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;

A runtime, coreTeaser è esattamente la stessa istanza di TeaserImpl (o della classe interna usata dal Core Component) che sarebbe stata adattata se il tuo componente proxy non esistesse — non ne stai reimplementando la logica, la stai avvolgendo.

Evitare il boilerplate con @Delegate di Lombok

Teaser estende Component e dichiara più di una dozzina di metodi (getTitle(), getPretitle(), getLink(), getImageResource(), isActionsEnabled(), getActions() e altri ancora, oltre a tutto ciò che eredita da Component). Scrivere a mano un metodo di inoltro per ognuno di essi solo per modificarne uno è esattamente il copia-incolla che questo pattern esiste per evitare.

Qui entra in gioco @Delegate di Lombok (lombok.experimental.Delegate — si trova nel package experimental di Lombok, ma è lo strumento standard usato dalla community AEM per questo scopo): posizionata sul campo coreTeaser, genera in fase di compilazione un metodo di inoltro per ogni metodo pubblico di Teaser. Per tenere un metodo per te, lo escludi con @Delegate(excludes = ...), puntando a una piccola interfaccia marcatore che dichiara solo la firma (o le firme) che stai sovrascrivendo — altrimenti il metodo di inoltro generato da Lombok e il tuo @Override entrano in collisione con un errore di compilazione di “metodo duplicato”.

Lombok stesso viene aggiunto come dipendenza con scope provided sul bundle core — serve solo in fase di compilazione per l’elaborazione delle annotazioni, non a runtime.

Un esempio completo: sovrascrivere come il Teaser sceglie la propria immagine

Teaser.getImageResource() (aggiunto in Core Components 12.4.0) può restituire null quando il teaser non ha un’immagine propria e nessuno dei suoi contenuti collegati ne fornisce una. Supponiamo che il design system del progetto imponga che un teaser non venga mai renderizzato senza immagine — in tal caso si ricorre a un asset di fallback condiviso:

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;

    /**
     * I metodi elencati qui sono esclusi dall'inoltro generato da
     * Lombok, così da poterli sovrascrivere qui sotto senza un errore
     * di compilazione di "metodo duplicato".
     */
    private interface Overrides {
        Resource getImageResource();
    }

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

Tutti gli altri metodi di MyTeasergetTitle(), getLink(), isActionsEnabled(), getExportedType() ereditato da Component, tutto — sono generati da Lombok e si limitano a inoltrare a coreTeaser. Hai scritto solo l’unico metodo che dovevi davvero cambiare.

Perché lo script HTL non deve cambiare

Poiché lo script HTL del componente proxy è a sua volta ereditato senza modifiche dal Core Component (è il meccanismo di sling:resourceSuperType lato HTL), continua a contenere qualcosa come data-sly-use.teaser="com.adobe.cq.wcm.core.components.models.Teaser" — adatta all’interfaccia, non a una classe concreta. Sling Models decide quale implementazione @Model registrata usare per quell’interfaccia in base al sling:resourceType effettivo della resource, preferendo la corrispondenza più vicina quando più di un model dichiara la stessa interfaccia adapter. Per una resource il cui sling:resourceType è myproject/components/teaser, quella è MyTeaser — quindi lo script HTL ereditato inizia a mostrare la tua logica di immagine di fallback senza che una sola riga di markup cambi.

Errori comuni nei progetti AEM reali