bloggers bloggers

Marco Napolitano
Messaggi: 79
Stelle: 0
Data: 17/02/22
Jader Jed Francia
Messaggi: 63
Stelle: 0
Data: 18/02/21
Paolo Gambetti
Messaggi: 2
Stelle: 0
Data: 11/11/19
Katia Pazzi
Messaggi: 1
Stelle: 0
Data: 27/06/19
Ezio Lombardi
Messaggi: 11
Stelle: 0
Data: 10/04/18
Chiara Mambretti
Messaggi: 25
Stelle: 0
Data: 27/02/17
Serena Traversi
Messaggi: 3
Stelle: 0
Data: 21/07/16
Francesco Falanga
Messaggi: 8
Stelle: 0
Data: 14/06/16
Antonio Musarra
Messaggi: 2
Stelle: 0
Data: 18/11/13
Simone Celli Marchi
Messaggi: 6
Stelle: 0
Data: 09/07/13
Indietro

Liferay 7.1 e ServiceTracker: nuovi amici utili e molto comodi! ;)

Ciao a tutti!

Ammetto che mi fa un po' strano scrivere di nuovo sul blog smiley, soprattutto perché qualcuno che ha sicuramente più tempo di me per farlo in effetti ha preso un bel distacco sul numero di post effettuati! 

Visto che però, finalmente, anche io ho trovato argomenti interessanti da postare :D, ho pensato di rifarmi vivo per condividere con voi quello che ho scoperto giocando con Liferay 7.1!

Intanto una premessa: come al solito la vita di Jader è costellata di animali mitologici, spesso impegnati a far fare a Jader cose strane.. Come sempre vi chiedo indulgenza nel non deridermi sul perché queste creature mi chiedano certe cose ;), ma di concentrarvi sul caso funzionale e di commentare, se lo ritenete opportuno, l'implementazione tecnica!

Venendo quindi all'oggetto del post, vi vado ad introdurre i miei nuovi amici, legati al mondo OSGi, che mi si è aperto con l'introduzione di Liferay 7: i Service Tracker.

Service Tracker: cosa sono e a cosa servono?

Allora, la prima cosa che vorrei spiegarvi è cosa sono e a cosa servono i service tracker nell'architettura OSGi, così da introdurveli e farvi comprendere come possono tornarci comodi per le nostre implementazioni su Liferay!

Stranamente :), per comprenderli bene vi consiglierei di partire dalla documentazione ufficiale di Liferay che, per una volta :), non è così incompleta, buggata e {fair mode="on"}"non così utile"{/fair} come siamo abituati a ricordarcela! :)

Per chi fosse davvero pigro (o avesse poca dimestichezza con il mondo OSGi), il concetto lo si può riassumere davvero mooooolto velocemente così: i ServiceTracker sono gli oggetti che ci permettono di accedere al registry OSGi, interrogandolo a runtime sui servizi disponibili.

Significa che questi oggetti sono il modo con cui possiamo:

  • interrogare il container OSGi dicendogli "dammi tutti i servizi di un certo tipo";
  • fare qualche cosa a runtime su questi servizi (cosa poi lo vediamo dopo.. ;))

E qui già è spiegata parte della magia: i ServiceTracker ci permettono, a runtime, di sapere se / quali servizi sono presenti all'interno del motore.

Ora voi ci chiederete perché una persona normale dovrebbe voler fare questo tipo di domanda al container, quando è noto dalla notte dei tempi che è possibile farseli iniettare senza troppo sforzo; beh, qui entriamo subito nel secondo pezzo dell'articolo!

Caso d'uso: wizard di raccolta dati di un'anagrafica

Ok, confesso che questo titolo non rende giustizia al caso specifico; detta così scomodare addirittura un ServiceTracker potrebbe sembrare inutile perché, lo abbiamo fatto tutti, un wizard potrebbe banalmente essere una sequenza di JSP che si richiamano tra di loro (in catena visto che è un wizard... :)) e che, una volta arrivato in fondo termina senza troppo sforzo.

Per capire bene il mio caso d'uso, però, vi devo aggiungere dei dettagli funzionali che renderanno un po' più pepata la situazione e giustificheranno quindi l'utilizzo dei ServiceTracker all'interno del modello che andremo a realizzare!

Il primo requisito che i miei animali mitologici mi hanno chiesto è, nemmeno a dirlo, il disaccoppiamento! Il wizard che dovremo creare dovrà quindi permettere loro di caricare bundle nel container e aggiungere così pagine al wizard. E qui, l'implementazione monolitica non è molto furba, considerando il fatto che loro hanno specificato chiaramente che vogliono aggiungere bundle, e non "modificare e rilasciare" sempre lo stesso con il wizard...

Anzi: nella loro testa il wizard è solo un visualizzatore: quello che visualizza dovrebbe essere proprio discriminato dai vari bundle, che successivamente, questi caricheranno.

Il secondo requisito che mi hanno dato è stato l'ordinamento: per definizione un wizard è composto di una serie arbitraria di pagine in sequenza. Quindi loro vorrebbero che il wizard fosse in grado, da solo, di discriminare l'ordine di visualizzazione (e di conseguenza anche quello di compilazione da parte dell'utente finale) delle singole pagine.

Infine, l'ultimo requisito è relativo al comportamento: per farla facile, non tutte le pagine devono essere compilate da tutti gli utenti, quindi il wizard deve essere in grado di mostrare o meno la singola pagina a seconda di regole arbitrarie che saranno  codificate nei singoli bundle rilasciati.

Architettura applicativa

Siccome non ho mai niente da fare perché, come molti di voi purtroppo sanno :), io per lavoro non lavoro :), avevo sentito parlare dei ServiceTracker e quindi, anche un po' per curiosità :), ho proposto di implementare il wizard in questo modo:

  1. un bundle relativo al wizard vero e proprio (quindi la Portlet da mettere in pagina);
  2. un bundle relativo all'interfaccia del servizio (che chiameremo WizardController);
  3. una serie di bundle che, implementando l'interfaccia WizardController, ci permetteranno di caricare a runtime le pagine del wizard.

Ovviamente, dietro a tutto questo, nella mia testa c'era che il ServiceTracker mi avrebbe aiutato a realizzare la magia. E tra un po' vedremo come.. ;)

Comunque sia, grazie a questa modularizzazione, ho pensato, abbiamo risolto il requisito numero uno, quello relativo al disaccoppiamento.

Infatti, se tutto andrà come voluto ;), avremo effettivamente la possibilità di caricare bundle a raglio nel container OSGi e popoleremo in maniera disaccoppiata il nostro wizard. Questo perché, grazie al ServiceTracker, potremo modificare a runtime la nostra chain ogni volta che un bundle sarà aggiunto / rimosso dal container!

Però, a meno di non cablare da qualche parte nella Portlet la logica di sorting delle pagine, non abbiamo ancora risolto il secondo requisito, quello relativo all'ordinabilità delle pagine.

Per farlo in maniera furba (almeno: questo è quello che ho pensato io, ma sono aperto anche a soluzioni differenti), ho modellato il concetto di "catena di pagine" attraverso un oggetto che ho chiamato convenzionalmente WizardChain. Come potete ben intuire :), questo oggetto ha la responsabilità di rappresentare la catena delle pagine del wizard, così che io possa inserirvi all'interno la logica di sorting e quindi, all'occorrenza, sostituirla fornendone un'implementazione differente!

Ancora una volta il requisito del disaccoppiamento è rispettato!

Rimane da gestire il terzo requisito, quello del comportamento. Qui però la cosa è semplice: come avrete sicuramente già intuito, modellando con dei Servizi i singoli anelli della catena, il comportamento è dettato:

  • in parte da come la Portlet interpreta il Servizio;
  • in parte da come il Servizio è implementato.

E adesso.. Codice!! :)

Bene! Finalmente possiamo dare libero sfogo al codice, illustrandovi un po' come fare a gestire tutto quello che ho descritto qui sopra su Liferay 7.1.

Partiamo dalla parte facile: la definizione del Servizio.

Qui l'ho giocata facile, un Servizio, in termini assoluti, non è che l'implementazione di una interfaccia! E allora ecco la nostra interfaccia:

package it.dvel.playground.wizard.chain;

import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.model.User;
import com.liferay.portal.kernel.service.ServiceContext;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletException;
import java.io.IOException;

public interface WizardController {

    /**
     * Restituisce il path della JSP che visualizza il singolo passo del wizard.
     * @return La JSP da visualizzare all'interno del wizard.
     */
    public String getJspPath();

    /**
     * Vale <code>true</code> se il passo &egrave; gi&agrave; stato completato; <code>false</code> altrimenti.
     * @param user l'utente che deve eseguire il passo del wizard.
     * @param ctx Il <code>ServiceContext</code> relativo alla richiesta.
     * @return <code>true</code> se il passo &egrave; gi&agrave; stato completato; <code>false</code> altrimenti.
     * @throws PortalException In caso di errori nell'elaborazione.
     */
    public boolean isCompleted(User user, ServiceContext ctx) throws PortalException;

    /**
     * Determina se il passo &egrave; applicabile all'utente corrente o meno.
     * @param user l'utente che deve eseguire il passo del wizard.
     * @return ritorna <code>true</code> se l'utente pu&ograve; eseguire il passo, <code>false</code> altrimenti.
     */
    public boolean isValid(User user);

    /**
     * Processa la logica di business per il salvataggio dei dati del singolo step.
     * @param request La <code>ActionRequest</code> di richiesta.
     * @param response La <code>ActionResponse</code> di risposta.
     * @throws PortletException In caso di errori nell'elaborazione (controllati o meno).
     * @throws IOException In caso di errori nell'elaborazione (controllati o meno).
     */
    public void processLogic(ActionRequest request, ActionResponse response)  throws PortletException, IOException;

    /**
     * Determina se una eccezione &egrave; o meno gestita all'interno della pagina.
     * @param cause Ritorna <code>true</code> se l'eccezione &egrave; controllata a livello di pagina, <code>false</code> altrimenti.
     * @return <code>true</code> se l'eccezione &egrave; controllata a livello di pagina, <code>false</code> altrimenti.
     */
//    public boolean isSessionError(Throwable cause);

}

Senza girarci troppo intorno ;), ho definito un'interfaccia applicativa che supporti tutti i comportamenti che i requisiti mi avevano espresso; faccio notare che, volutamente, manca l'ordinamento, perché questa sarà una sorpresa che gestiremo attraverso il ServiceTracker più avanti.

Adesso invece passiamo a vedere com'è fatta la WizardChain, che vi ricordo essere l'oggetto che rappresenta tutti gli anelli della catena del nostro wizard.

package it.dvel.playground.wizard.chain;

import com.liferay.osgi.service.tracker.collections.list.ServiceTrackerList;
import com.liferay.osgi.service.tracker.collections.list.ServiceTrackerListFactory;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.util.GetterUtil;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;

import java.util.LinkedList;

@Component(
        immediate = true,
        service = WizardChain.class
)
public class WizardChain extends LinkedList<WizardController> {

    public WizardChain() {
    }


    public void init() {
        // Ogni volta che reinizializzo la chain, prima la svuoto..
        if (size() > 0) {
            removeRange(0, size());
        }

        // poi la ripopolo...
        for (WizardController controller : _wizardControllers) {
            if (_log.isDebugEnabled())
                _log.debug("Addin' controller named " + controller.getClass().getCanonicalName() + " to wizard");
            add(controller);
        }
    }

    @Activate
    protected void activate(BundleContext bundleContext) {

        _wizardControllers = ServiceTrackerListFactory.open(
                bundleContext, WizardController.class, (o1, o2) -> {
                    if (GetterUtil.getLong(o1.getProperty("dvel.wizard.screen.number"), 10) < GetterUtil.getLong(o2.getProperty("dvel.wizard.screen.number"), 10)) {
                        return -1;
                    } else {
                        return 1;
                    }
                });
    }

    @Deactivate
    protected void deactivate() {

        if (_wizardControllers != null) {
            _wizardControllers.close();
        }
    }

    private ServiceTrackerList
            <WizardController, WizardController>
            _wizardControllers;

    public static final Log _log = LogFactoryUtil.getLog(WizardChain.class);
}

Qui abbiamo già svelato buona parte della magia!

Come potrete notare, infatti, qui sono risolti:

  • il "segreto" del ServiceTracker;
  • il "segreto" del sorting dei servizi.

Ma vediamo nel dettaglio quanto sopra, così da coglierne i punti interessanti.

Partiamo subito facendo notare che qui stiamo creando tecnicamente un Component; questo aspetto è importante perché trasforma, di fatto, il nostro oggetto in un servizio OSGi che potremo in futuro -volendo- sovrascrivere. Su questo tema si sono spese in questi anni miliardi di parole / righe di codice, quindi soprassiedo perché non credo sia importante. Se per qualcuno lo fosse, scrivetelo nei commenti che poi lo spiego! ;)

Il secondo aspetto è che per l'implementazione della mia chain ho scelto -non a caso- di estendere una LinkedList; anche questo aspetto è voluto: siccome pensavo potesse tornarmi comodo gestire la catena a partire da un singolo anello; ho pensato potesse essere comodo avere una struttura dati che rappresentasse, appunto, il concetto di "lista di elementi tra loro collegati".

Ho quindi scelto di utilizzare una LinkedList, che fa esattamente quello che volevo! ;)

Se saltiamo in fondo alla classe, poi, troveremo, subito prima della definizione del classico Log (digressione: ancora non mi capacito come, per uno che ha scritto 10milioni di righe di codice praticamente da solo, una costante static e final si chiami convenzionalmente con il nome di un field interno di classe, ma tant'è..), troviamo questo esoterico pezzo di codice:

    private ServiceTrackerList
            <WizardController, WizardController>
            _wizardControllers;

Da un punto di vista implementativo, questa è la prima magia!

Questa è di fatto una implementazione di ServiceTracker fornita out-of-the-box direttamente da Liferay.

Io ero partito pensando di scrivermi, come suggerito dalla documentazione che cito all'inizio del post ;), un ServiceTracker custom; poi ravanando nel codice sorgente di Liferay 7.1, ho scoperto il mondo dei ServiceTracker già implementati e il gioco è stato semplice! Anche perché, ho pensato, vuoi che il buon Brian non abbia pensato e me e non mi abbia già messo a disposizione tutto quello che mi serve?? Infatti è così: come sempre lavorare con Liferay, se conosci bene un milione di framework / concetti / infastruttura / codice / quello-che-preferisci è davvero piacevole!! C'è sicuramente già tutto quello che ti serve.. Se sai dove andarlo a cercare!! :D

Ma torniamo alla nostra implementazione! Dicevo.. Mi sono scelto quello che faceva di più al caso mio (quindi una lista di Service e non, banalmente, un Singleton) e l'ho utilizzata!

Hint! Se anche voi siete curiosi di vedere quali e quanti ServiceTracker Liferay 7.1 vi mette a disposizione OOTB, vi consiglio di guardare il contenuto del package com.liferay.osgi.service.tracker.collections.list e dare un occhio attento anche alle varie Factory sparse al suo interno.. ;)

Arrivati a questo punto, però, vi devo spiegare un attimino (moooolto a grandi linee) come funziona il cinema dei ServiceTracker, lato OSGi, altrimenti il resto dell'implementazione non è molto chiara!

Allora, il giro prevede che tu possa aprire e chiudere una sorta di listener sul registry OSGi all'interno del quale sarà poi aggiunto / rimosso dinamicamente dal motore stesso un singolo servizio. Per fare questa magia, ovviamente, utilizziamo una factory messa a disposizione da Liferay che ci consente di nascondere tutto il codice ridondante che ci vorrebbe per istanziare un ServiceTracker e che ci permette, invece, di ottenerlo molto più velocemente e con un dispendio di codice quasi pari a zero.

A questo proposito, quindi, utilizzo per inizializzare il nostro ServiceTracker l'oggetto ServiceTrackerList questa chiamata:

_wizardControllers = ServiceTrackerListFactory.open(
                bundleContext, WizardController.class, (o1, o2) -> {
                    if (GetterUtil.getLong(o1.getProperty("dvel.wizard.screen.number"), 10) < GetterUtil.getLong(o2.getProperty("dvel.wizard.screen.number"), 10)) {
                        return -1;
                    } else {
                        return 1;
                    }
                });

Questa è un po' da argomentare anche se, immagino, i più sgami di voi avranno già intuito tutto! :)

Allora, la ServiceTrackerListFactory, attraverso il metodo statico open() ci permette, appunto, di ottenere una reference al ServiceTracker che ci serve. ServiceTracker che, faccio notare, è tipato sul Servizio che vogliamo ascoltare.

E nel nostro caso specifico, il ServiceTracker che ci serve è, ovviamente un ServiceTrackerList (infatti lo istanziamo una ServiceTrackerListFactory.. :)) ma, al metodo open(), specifichiamo:

  • il bundleContext nel quale siamo;
  • il tipo di servizi che il ServiceTracker deve ascoltare (nel nostro caso, appunto, i WizardController);
  • il metodo di sort per ordinarli che, udite udite ;), IO ho scelto di implementare con una lambda (lo so: io ero più incredulo di voi, ma quando ho capito che la parallel execution è a un passo da me ho capito che potevo fidarmi anche io delle lambda.. ;))

Nello specifico, tutto questo avviene all'interno di un metodo annotato con una annotation che è Activate che, nemmeno a dirlo, permette di far invocare il metodo annotato all'attivazione del bundle.

Si: è un modo alternativo per definire il bundle activator senza definirlo esplicitamente nel descrittore OSGi..

La lambda comunque è semplice: mi aspetto che ogni Servizio che ricevo abbia tra le sue properties la property dvel.wizard.screen.number sulla quale, poi, faccio il classico sort utilizzando la logica del Comparator. In questo modo riesco a risolvere anche il requisito numero tre, ovvero quello dell'ordinabilità dei componenti all'interno del wizard sempre, requisito numero uno, in maniera disaccoppiata.

E il bello è che la cosa, grazie alle facility messe a disposizione dagli oggetti di Liferay, è semplice come.. Passare una lambda che implementa il Comparator! :)

A questo punto il più è fatto: abbiamo una reference interna al nostro service che è la chain che viene mantenuto costantemente aggiornato dal motore OSGi, a runtime, e nel quale vengono aggiunti / rimossi i servizi che stiamo ascoltando senza che noi si debba fare nulla.

L'unica accortezza che dobbiamo avere, pena una tediosissima memory leak legata alla non garbadgiabilità della classe ;), è quella di chiudere il listener quando il modulo viene rimosso / spento / disattivato dal container; lo facciamo, banalmente, qui:

    @Deactivate
    protected void deactivate() {

        if (_wizardControllers != null) {
            _wizardControllers.close();
        }
    }

Non credo ci sia bisogno di commentare questo codice, quindi su questo soprassiedo.

Ultimo pezzo da vedere, l'implementazione della Portlet che poi piloterà tutto il giro!

Ed ecco qui l'ultimo pezzo del nostro trittico:

package it.dvel.playground.wizard.portlet.portlet;

import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.model.User;
import com.liferay.portal.kernel.portlet.bridges.mvc.MVCPortlet;
import com.liferay.portal.kernel.service.ServiceContext;
import com.liferay.portal.kernel.service.ServiceContextFactory;
import com.liferay.portal.kernel.service.UserLocalService;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.kernel.util.WebKeys;
import it.dvel.playground.util.PortletKeys;
import it.dvel.playground.wizard.chain.WizardChain;
import it.dvel.playground.wizard.chain.WizardController;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import javax.portlet.*;
import java.io.IOException;

/**
 * @author jed
 */
@Component(
        immediate = true,
        property = {
                "com.liferay.portlet.display-category=category.dvel.playground",
                "com.liferay.portlet.instanceable=false",
                "javax.portlet.init-param.template-path=/",
                "javax.portlet.init-param.view-template=/view.jsp",
                "javax.portlet.name=" + PortletKeys.WIZARD,
                "javax.portlet.display-name=" + PortletKeys.WIZARD_DISPLAY_NAME,
                "javax.portlet.resource-bundle=content.Language",
                "javax.portlet.security-role-ref=power-user,user"
        },
        service = Portlet.class
)
public class WizardPortlet extends MVCPortlet {

    public void processLogic(ActionRequest actionRequest, ActionResponse actionResponse) throws IOException, PortletException {
        WizardController controller = getWizardController(actionRequest);

        if (Validator.isNotNull(controller)) {
            controller.processLogic(actionRequest, actionResponse);
        }
    }

    private WizardController getWizardController(PortletRequest request) throws PortletException {
        try {
            ServiceContext ctx = ServiceContextFactory.getInstance(request);
            User user = userLocalService.getUser(ctx.getUserId());
            chain.init();
            for (int i = 0; i < chain.size(); i++) {
                WizardController controller = chain.get(i);
                if (controller.isValid(user) && !controller.isCompleted(user, ctx)) {
                    return controller;
                }
            }
            return null;
        } catch (PortalException e) {
            throw new PortletException(e.getMessage(), e);
        }
    }

    @Override
    public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException {
        WizardController controller = getWizardController(renderRequest);

        if (Validator.isNotNull(controller)) {
            include(controller.getJspPath(), renderRequest, renderResponse);
        } else {
            renderRequest.setAttribute(WebKeys.PORTLET_CONFIGURATOR_VISIBILITY, Boolean.FALSE);
            include(viewTemplate, renderRequest, renderResponse);
        }

    }

    @Reference
    private UserLocalService userLocalService;

    @Reference
    private WizardChain chain;

    public static final Log _log = LogFactoryUtil.getLog(WizardPortlet.class);
}

Come immagino vi sareste aspettati, qui non c'è nessuna magia:

  • creo un Component di tipo Portlet (maddai.. :))
  • mi faccio iniettare una reference della mia WizardChain (così che in future release io possa sovrascriverla agilmente via OSGi)
  • mappo i metodi della mia Portlet (render() e processAction()) sui metodi dei singoli WizardController, così da poter gestire tutto il processo in modalità disaccoppiata.

Unico dettaglio implementativo: quando l'utente ha completato il wizard, il requisito era che la portlet non fosse nemmeno visibile sulla pagina; questo spiega perché nel render() c'è questa implementazione: 

if (Validator.isNotNull(controller)) {
            include(controller.getJspPath(), renderRequest, renderResponse);
        } else {
            renderRequest.setAttribute(WebKeys.PORTLET_CONFIGURATOR_VISIBILITY, Boolean.FALSE);
            include(viewTemplate, renderRequest, renderResponse);
        }

Di fatto, se ho un controller delego ad esso la view altrimenti, come da requisito, nascondo la portlet!

Direi che questo è tutto, ora tocca a voi! Vi è mai capitato di avere a che fare con animali mitologici come i miei? :) Scontrarvi con requisiti funzionali di questo tipo o, più in generale, avete mai avuto bisogno di scomodare un ServiceTracker?

Oppure, più semplicemente, voler scrivere codice molto più riusabile, indipendente e disaccoppiato rispetto al classico monolite??

Nel caso in cui a una di queste domande abbiate risposto si, beh, ora avete una strada da investigare per vedere di riuscire a divertirvi mentre fate ciò che dovete con Liferay!

Come sempre, rimango in attesa di avere i vostri feedback; soprattutto sull'implementazione.

Confesso che ci sono cose sulle quali anche io sono stato titubante e, in partenza, avevo pensato di modellarle diversamente. Poi per tutta una serie di ragioni (la prima.. Il tempo!! :)), ho preferito implementare le cose così; però, se voi a differenza di me avete più tempo e voglia di regalarmi i vostri 2c, sarò ben felice di ascoltare un punto di vista differente!

Fino ad allora, come si dice in questi casi, happy coding e.. Lunga vita ai ServiceTracker!!! :D

Alla prossima, ciao, J.

Commenti
Nessun commento. Vuoi essere il primo.