Cookies help us deliver our services.

By using our services, you agree to our use of cookies. Learn more

I understand

D'vel Blog

By the end of the 2010 I work as Liferay Expert for D'vel snc, for which I often write technical articles on its blog.

This blog is really interesting and full of articles; if you're curious, this is the link: blog.d-vel.com.

Here is a list of the latest articles written.

blogdvel
  1. Correva l'anno 2009 quando una giovane Serena scriveva un interessante post sulla modalità con cui valorizzare i placeholder all'interno di un PDF; per chi se lo fosse perso vi invito a leggere il post originale.

    Sono passati ormai un pò di anni, i sistemi sono cambiati ma le esigenze sono rimaste le stesse ed infatti oggi mi sono ritrovato a dover fare la medesima cosa: valorizzare i placeholder all'interno di un PDF.

    Il post originale è stato di grande ispirazione ma purtroppo alcune librerie sono cambiate ed ho dovuto fare qualche modifica al codice; modifica che voglio condividere con voi... e con me stesso quando tra qualche mese dovrò rifarlo nuovamente!

    Innanzitutto io sto lavorando con Liferay 7.1.3 e di conseguenza la prima cosa da fare è configurare correttamente tutte le dipendenze. Nel mio caso utilizzerò solamente alcuni pacchetti forniti dalla libreria iText7; pertanto aggiungiamo le seguenti righe all'interno del file build.gradle:

     compileInclude group: "com.itextpdf", name: "kernel", version: "7.1.6" compileInclude group: "com.itextpdf", name: "forms", version: "7.1.6"

    Queste sono le uniche librerie che ci servono perchè l'unica cosa che dobbiamo fare è valorizzare i placeholder all'interno di un PDF che già esiste; la versione 7.1.6 è la più recente al momento in cui sto scrivendo questo post.

    In realtà queste librerie si portano dietro altre dipendenze, che a noi non servono, pertanto andiamo anche ad inserire le seguenti righe all'interno del file bnd.bnd:

     Import-Package: \ !org.bouncycastle.*,\ !org.slf4j.impl.*,\ *

    A questo punto facciamo un bel "gradle refresh" e deployamo tutto per verificare che le dipendenze siano tutte soddisfatte.

    Veniamo ora al codice da usare per valorizzare i placeholder e salvare il file di output; siccome stiamo parlando di file e di Liferay, diamo per scontato che il codice seguente sia inserito all'interno del metodo serveResource della nostra portlet in modo da consentirci di salvarlo.

     import com.itextpdf.forms.PdfAcroForm; import com.itextpdf.forms.fields.PdfFormField; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfReader; import com.itextpdf.kernel.pdf.PdfWriter; // Recupero dalla Document Library del PDF contenente i placeholder FileEntry fileEntry =... // Creazione reader del PDF PdfReader reader = new PdfReader(fileEntry.getContentStream()); // File di output File file = FileUtil.createTempFile(); PdfWriter writer = new PdfWriter(file); // Creazione del documento PdfDocument pdfDocument = new PdfDocument(reader, writer); // Form contenente i campi del PDF da riempire PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDocument, true); form.setGenerateAppearance(true); // Recupero e valorizzazione dei placeholder Map<String, PdfFormField> fields = form.getFormFields(); fields.get("nome_campo_1").setValue("..."); fields.get("nome_campo_1").setValue("..."); fields.get("nome_campo_1").setValue("..."); // Valorizzare dei placeholder rimanenti // Rende i placeholder del PDF non modificabili form.flattenFields(); pdfDocument.close(); writer.close(); reader.close(); String fileName = fileEntry.getFileName(); // Download del file PortletResponseUtil.sendFile( resourceRequest, resourceResponse, fileName, FileUtil.getBytes(file), MimeTypesUtil.getContentType(file));
    Chiaramente potete anche decidere si salvare il file di output nella Document Libray anzichè farne il download...
  2. Fortunamente non ho animali mitologici da gestire come Jader, tuttavia però... ho Jader :)

    Oggi voglio parlare un pò di OSGi illustrandovi una soluzione molto elegante per risolvere un problema che potrebbe capitarvi.

    Il requisito iniziale, a parole, è molto semplice: rimuovere il link Password dimenticata dalla portlet di login nativa di Liferay 7.1 ed aggiungere un link custom che punti ad una pagina in cui è presente una portlet custom.

    In sostanza il risultato finale deve essere il seguente (sotto al pulsante Accedi):

    Soddisfare il primo requisito (rimuovere il link Password dimenticata) è semplice perchè si tratta di una configurazione da fare sull'istanza di Liferay.

    Invece per aggiungere un link custom la cosa è un pochino diversa; se fossimo sulla versione 6.2 avrei già iniziato a parlare di hook della JSP di portale e sinceramente il mio approccio iniziale era di fare qualcosa di analogo anche sulla 7.1. Così ho iniziato a cercare nei sorgenti di portale il punto in cui venivano gestiti i link ed ho scoperto che... non venivamo gestiti! Non esplicitamente almeno. Cioè?!

    Il file in cui vengono gestiti i link a fondo pagina nella portlet di login è il seguente:

     [SRC]\modules\apps\login\login-web\src\main\resources\META-INF\resources\navigation.jspf

    Ma al suo interno non ci sono link da modificare ma solamente taglib del tipo:

     <liferay-util:dynamic-include key="com.liferay.login.web#/navigation.jsp#post" />

    In pratica, per farla breve, tutti i link presenti vengono inclusi dinamicamente a runtime (dynamic-include per l'appunto); pertanto non bisogna modificare la portlet di login ma semplicemente realizzare un opportuno componente OSGi che venga caricato a runtime dalla taglib e si occupi di renderizzare il link custom.

    Non mi soffermo oltre e vi mostro qualche riga di codice!

     @Component(immediate = true, property = { "login.web.navigation.position=post", "service.ranking:Integer=100" }, service = PageInclude.class) public class MyNavigationPostPageInclude implements PageInclude { @Reference private LayoutLocalService _layoutLocalService; @Override public void include(PageContext pageContext) throws JspException { try { // La request serve sempre... HttpServletRequest request = (HttpServletRequest) pageContext.getRequest(); // Sito di riferimento long groupId = PortalUtil.getScopeGroupId(request); // Recupero la pagina a cui deve puntare il link custom Layout layout = _layoutLocalService .fetchLayoutByFriendlyURL(groupId, false, "/my-friendly-url"); if (layout != null) { /* * Se il testo del link non è una traduzione di portale, allora * è necessario recuperarla esplicitamente dal vostro bundle */ Locale locale = PortalUtil.getLocale(request); ResourceBundle resourceBundle = ResourceBundleUtil.getBundle(locale, this.getClass()); String message = LanguageUtil.get( resourceBundle, "link-text-label"); // Creazione della render URL da associare al link custom LiferayPortletURL myURL = PortletURLFactoryUtil.create( request, PortletKeys.MY_CUSTOM_PORTLET, layout, PortletRequest.RENDER_PHASE); // Istanzio e configuro via codice la taglib aui:icon IconTag iconTag = new IconTag(); iconTag.setCopyCurrentRenderParameters(false); iconTag.setIconCssClass("icon-undo"); iconTag.setLocalizeMessage(false); iconTag.setMessage(message); iconTag.setUrl(myURL.toString()); iconTag.doTag(pageContext); } } catch (PortalException e) { throw new JspException(e); } } }
    Non so voi, ma io la trovo una soluzione estremamente elegante che evita di mettere mano ai sorgenti di portale.
  3. Buongiorno a tutti e ben trovati sul nostro blog! ;)

    Il post di oggi sarà super rapido e semplicissimo: di fatto, come spiego anche nell'intro, mi sono trovato a dover centralizzare taglib che vivevano in bundle differenti all'interno di un unico bundle centralizzato.

    Fin qui la cosa potrebbe sembrare semplice: fai un po' di copia / incolla / trascina (per i più sofisticati potrei definirlo refactor.. ;)) ma poi.. Fai deploy e scopri che non funziona più nulla!

    Come mai? Perché? Cosa NON ho copiato? Cosa ho sbagliato?

    Prima di farmi attanagliare da tutti questi dubbi, come spesso faccio :), mi sono buttato a capofitto nei sorgenti di Liferay 7.1 e ho scoperto che la cosa, da sistemare, era davvero semplice!

    bnd.bnd: come sempre, tutto parte da qui!

    Anche questa volta il titolo del capitolo non è molto evocativo ;), però è un periodo durante il quale la mia creatività è bassa.. Non me ne vogliate!

    Però il titolo è molto vero; infatti la prima cosa da fare è modificare il file bnd.bnd (che poi altro non è che il responsabile di quello che finisce nel manifest del nostro bundle OSGi) per dirgli:

    • che, ovviamente, fornisce come servizio delle taglib;
    • che abbiamo più di una taglib all'interno del bundle.

    Per fare questa magia, quindi, ho modificato così il mio bnd.bnd:

     Bundle-Name: dvel-util-taglib Bundle-SymbolicName: it.dvel.playground.util.taglib Bundle-Version: 1.0.0 Export-Package: \ it.dvel.playground.util.taglib.search.tag,\ it.dvel.playground.util.taglib.rubrica.tag Provide-Capability:\ osgi.extender;\ osgi.extender="jsp.taglib";\ uri="http://www.d-vel.com/tld/dvel";\ version:Version="${Bundle-Version}",\ osgi.extender;\ osgi.extender="jsp.taglib";\ uri="http://www.d-vel.com/tld/rubrica";\ version:Version="${Bundle-Version}" Web-ContextPath: /dvel-taglib

    La parte interessante è chiaramente quella che riguarda la property Provide-Capability; a questa, infatti, faccio corrispondere una lista di osgi.extender dove, per ognuno di essi, specifico la uri della singola taglib.

    Fatto questo il problema è risolto! Ora non ci resta che usare il nostro modulo e quindi le nostre taglib!

    Come fare? Semplice: nelle singole JSP dove ci servono, ci basterà importare la taglib (sempre nella init.jsp, mi raccomando!):

     <%@ taglib prefix="rubrica" uri="http://www.d-vel.com/tld/rubrica" %>

    e poi.. Utilizzarla:

     <rubrica:listUtil var="listUtil" />

    Come detto, questo post è super veloce e molto semplice.. Se sapevate come fare questa cosa!!

    Spero di avervi dato un buon suggerimento; se vi occorrono maggiori dettagli o siete semplicemente curiosi di conoscere qualche altro dettaglio su questo tema, come sempre, usate l'area dei commenti e saremo ben lieti di aiutarvi!!

    A presto e buon divertimento con Liferay 7.1, le taglib (centralizzate tutte in un unico modulo) e i casi funzionali degli animali mitologici più strani che ci siano! :)

    Alla prossima!!

  4. Buongiorno a tutti!

    Eccomi di nuovo da voi per un post su Liferay 7.1 e le magie di OSGi!

    Anche oggi vorrei raccontarvi quello che i miei tanti animali mitologici mi hanno chiesto qualche tempo fa e come sono riuscito a risolvere il problema!

    Per farlo, però, come sempre vi devo raccontare il caso d'uso, così che l'implementazione possa essere quantomeno sensata.. :)

    Caso d'uso: codice centralizzato ma.. Specializzato per ogni modulo!

    Lo so: la cosa più assurda dei miei post sono i requisiti che i miei animali mitologici riescono a darmi! :)

    Però credetemi: quando me li danno me li motivano e, almeno sulla carta, sembra che abbiano pure un senso! 

    Nello specifico, questa volta, quello che mi hanno chiesto è stato proprio.. Creare del codice che dovrà essere centralizzato (per ovvie ragione) ma che.. Debba essere specializzato per ogni modulo! Il motivo è semplice:

    • centralizzato: perché alla fine la logica è uguale in tutti i punti dove viene utilizzato;
    • specializzato: perché vogliono predisporsi al cambiamento; nel caso la logica standard non vada più bene in un modulo, vogliono semplicemente cambiarne l'implementazione in quel modulo e tutto deve continuare a funzionare.

    Come al solito, nemmeno a farlo apposta, tutti a me capitano.. :D

    Ora che abbiamo definito che i miei animali mitologici sono particolari :), andiamo a vedere come ho risolto questi due vincoli, sfruttando una delle caratteristiche di OSGi che usate spesso ma alla quale, magari, non avete mai prestato particolare attenzione..

    Coding time!

    Partiamo subito facendo vedere il codice che ho centralizzato; qui, ovviamente, non mi soffermo sul cosa, perché è abbastanza indifferente. Mi concentro di più sul come, che è la parte un po' più interessante dell'implementazione.

     package it.dvel.playground.search; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.search.SearchContext; import com.liferay.portal.kernel.search.filter.BooleanFilter; import com.liferay.portal.kernel.util.Validator; import com.liferay.portal.search.spi.model.query.contributor.ModelPreFilterContributor; import com.liferay.portal.search.spi.model.registrar.ModelSearchSettings; import it.dvel.playground.util.search.SearchContextConstants; import org.osgi.service.component.annotations.Component; import java.util.Date; @Component( immediate = true, property = "model.pre.filter.contributor.id=DateRangeFilter", service = ModelPreFilterContributor.class ) public class DateRangePreFilterContributor extends BaseDateRangePreFilterContributor { @Override public void contribute(BooleanFilter booleanFilter, ModelSearchSettings modelSearchSettings, SearchContext searchContext) { Date startDate = (Date) searchContext.getAttribute(SearchContextConstants.SEARCH_BY_FROM_DATE); Date endDate = (Date) searchContext.getAttribute(SearchContextConstants.SEARCH_BY_TO_DATE); if (Validator.isNotNull(startDate) && Validator.isNotNull(endDate)) { endDate = setTime(endDate, 23, 59); if (_log.isDebugEnabled()) { _log.debug("Start Date: " + startDate); _log.debug("End Date: " + endDate); } String field = (String) searchContext.getAttribute("it.dvel.playground.search.common.field.date.range"); addFilter(field, booleanFilter, startDate, endDate); } } private static final Log _log = LogFactoryUtil.getLog(DateRangePreFilterContributor.class); }

    Vi manca metà del mondo degli oggetti della modellazione, però avete l'unico che conta: l'oggetto che definisco centralizzato! Questo oggetto, che nel dettaglio serve a scatenare un filtro sulla ricerca attraverso l'indice nel caso siano presenti due date (lasciate stare il perché, dai.. :)), ha una particolarità: definisce un suo ID attraverso una property.

    Questa è la riga incriminata:

     property = "model.pre.filter.contributor.id=DateRangeFilter",

    Questa è la caratteristica interessante, e presto vedremo il perché!

    Injection di oggetti con.. Filtro!

    Il secondo requisito è quello della specializzazione: l'idea di base dei miei animali mitologici era quella di essere pronti al cambiamento! Siccome però, per definizione, gli animali mitologici sono come vogliamo vederli noi :), i miei erano pigri (almeno quanto me :)) e quindi volevano a tutti i costi che ogni modulo specializzato avesse:

    • una sua classe specifica per questo tipo di filtro;
    • che la classe specifica facesse allegramente uso dell'implementazione centralizzata.

    So che per i più questo, nel mondo del software ad oggetti, si chiama banalmente proxy, però qui la cosa si fa un po' più complicata, perché parliamo di Servizi rilasciati attraverso bundle differenti nel container OSGi.

    A questo punto.. Come fare a risolvere il requisito?

    Bhe, come dice il titolo di questo capitolo (e spoilerato poi anche dal dettaglio del capitolo precedente.. ;)), ci basterà solamente filtrare gli oggetti che ci vengono iniettati e il gioco sarà fatto!

    Ecco quindi come ho implementato l'oggetto all'interno del modulo specializzato, usando chiaramente il concetto di proxy sull'oggetto centralizzato!

     package it.dvel.playground.avvisi.search; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.search.SearchContext; import com.liferay.portal.kernel.search.filter.BooleanFilter; import com.liferay.portal.search.spi.model.query.contributor.ModelPreFilterContributor; import com.liferay.portal.search.spi.model.registrar.ModelSearchSettings; import it.dvel.playground.util.search.FieldConstants; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @Component( immediate = true, property = "indexer.class.name=it.dvel.playground.avvisi.model.Avviso", service = ModelPreFilterContributor.class ) public class AvvisiDateRangePreFilterContributor implements ModelPreFilterContributor { @Override public void contribute(BooleanFilter booleanFilter, ModelSearchSettings modelSearchSettings, SearchContext searchContext) { searchContext.setAttribute("it.dvel.plaground.search.common.field.date.range", FieldConstants.AVVISO_DATA_REDAZIONE); dateRangeFilterPreFilterContributor.contribute(booleanFilter, modelSearchSettings, searchContext); } @Reference(target = "(model.pre.filter.contributor.id=DateRangeFilter)") protected ModelPreFilterContributor dateRangeFilterPreFilterContributor; private static final Log _log = LogFactoryUtil.getLog(AvvisiDateRangePreFilterContributor.class); }

    La magia è tutta qui:

     @Reference(target = "(model.pre.filter.contributor.id=DateRangeFilter)") protected ModelPreFilterContributor dateRangeFilterPreFilterContributor;

    Nella Reference, infatti, io specifico indirettamente che vorrei un oggetto di tipo ModelPreFilterContributor però sono selettivo: non mi accontento di averne uno a caso ma voglio proprio quello che mi pare a me, quello che ha ID che vale DateRangeFilter.

    Ed è il container OSGi a fare la magia, popolandomi il field interno con l'oggetto specifico!

    Ovviamente questo tipo di implementazione sfrutta la caratteristica dell'iniezione selettiva degli oggetti fatta dal container OSGi, però, chiaramente, è basata su un assunto banale, ovvero che esista un solo ModelPreFilterContributor che ha quell'ID.

    Questo è un assunto molto forte, però è funzionale all'esempio che volevo mostrarvi!

    Adesso la palla è nel vostro campetto: se qualcuno ha idea di come rendere più robusta l'implementazione può scriverlo nei commenti, così da confrontarci e crescere tutti insieme, ovviamente migliorandoci! :)

    Fino ad allora, come al solito, divertitevi e fatemi sapere se vi torna tutto quello che ho scritto!

    Se avete domande o dubbi, potete scriverli nei commenti e sarò / saremo ben lieti di aiutarvi!!

    Buon divertimento a tutti con OSGi, Liferay 7.1 e i filtri sulle iniezioni degli oggetti! :)

    Materiale utile alla discussione

    • Javadoc dell'implementazione di Filter nel core di OSGi
    • La RFC1960 che rappresenta come i filtri di LDAP vadano costruiti (sintassi e logica): comodo perché è da qui che sono derivati  i filtri sugli oggetti nel container OSGi
  5. Ciao a tutti!

    Ammetto che mi fa un po' strano scrivere di nuovo sul blog smiley, soprattutto perché qualcunoche 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 chiudereuna 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.