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. Oggi vi parlerò di IFrame e di Javascript per un problema che ho avuto utilizzando Liferay ma che in realtà può essere applicato anche in altri contesti.

    La necessità che avevo era quella di visualizzare una pagina di Liferay 7.2 all'interno di un IFrame di una pagina di Liferay 6.2; inoltre avevo bisogno che l'IFrame si ridimensionasse in altezza automaticamente in modo da evitare il classico problema della doppia barra di scorrimento laterale.

    I più attenti di voi potrebbero dire: "Liferay ha già una portlet IFrame che fa esattamente quello che ti serve". Vero, peccato però che il ridimensionamento dell'IFrame non funzioni perchè vengono messi in atto dei controlli di sicurezza per cui Javascript non è in grado di recuperare dall'IFrame le informazioni necessarie per determinarne l'altezza; e nemmeno l'IFrame è in grado di invocare funzioni Javascript sul container esterno, per lo stesso motivo.

    Inoltre Liferay non consente che le sue pagine siano incluse in un IFrame (la famigerata Same Origin policy), quindi di problemi da risolvere ce ne sono diversi; ma procediamo per passi.

    Il primo problema che risolviamo è fare in modo che le pagine di Liferay 7.2 possano essere incluse in un IFrame; per questo dobbiamo creare il file system-ext.properties e posizionarlo nella cartella <tomcat>/webapps/ROOT/WEB-INF/classes. Il contenuto del file sarà l'elenco numerale di tutte le URL che devono essere incluse nell'IFrame, come ad esempio:

     http.header.secure.x.frame.options.0=/pagina-1 http.header.secure.x.frame.options.1=/web/guest/pagina-1 http.header.secure.x.frame.options.2=/en/page-1 http.header.secure.x.frame.options.3=/en/web/guest/page-2 http.header.secure.x.frame.options.4=... http.header.secure.x.frame.options.5=...

    A questo punto possiamo salvare il file, riavviare il server di Liferay 7.2 e finalmente riusciremo ad includere le specifiche pagine in un IFrame su Liferay 6.2.

    Bene, ma ora come facciamo a ridimensionare automaticamente l'IFrame in altezza? Per ottenere il risultato voluto dobbiamo riuscire a trasferire informazioni dall'IFrame al container. Per fare ciò dobbiamo scomodare Javascript ed in particolare la funzione postMessage che consente di inviare "messaggi" da una finestra ad un'altra del browser (anche non IFrame). Non entro nei dettagli della funzione ma se siete curiosi leggete qui.

    A questo punto, nella JSP della portlet da includere nell'IFrame aggiungiamo questo frammento di codice:

     <aui:script use="aui-base"> var content = A.one('#content'); var height = content.height(); parent.postMessage('{"height": ' + height + '}', '*'); </aui:script>

    Il codice non fa altro che recuperare l'elemento del DOM che rappresenta il contenuto della pagina e calcolarne l'altezza in pixel; dopodichè invia alla finestra parent (ossia il container dell'IFrame) un messaggio. Nel caso specifico il messaggio è una stringa che rappresenta un oggetto JSON con tutte le informazioni necessarie, nel nostro caso l'altezza in pixel; fate attenzione perchè la funzione postMessage consente di inviare anche oggetti ma, se non ricordo male, il buon vecchio IE supporta invece solamente stringhe.

    Ora possiamo spostarci su Liferay 6.2 dove dobbiamo andare ad intercettare il messaggio inviato dall'IFrame; il dove è ovviamente la JSP che visualizza l'IFrame e quindi avete 2 possibilità (nessuna delle quali verrà analizzata in questo post perché immagino siate già in grado di farlo da soli):

    1. fare un hook sulla portlet IFrame di Liferay 6.2
    2. farvi una vostra portlet custom per includere l'IFrame

    Quello che interessa a me adesso è solamente farvi vedere il codice per intercettare il messaggio:

     <aui:script use="aui-base"> var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message"; var listener = window[eventMethod]; // Ascolta i messaggi che arrivano dall'altra finestra listener(messageEvent, function(e) { var key = e.message ? "message" : "data"; // Contenuto del messaggio var data = e[key]; try { // Sapendo che il messaggio è in formato JSON, lo convertiamo var json = A.JSON.parse(data);  // Recupero del nodo del DOM che rappresenta l'IFrame var iframe = A.one('#<portlet:namespace />iframe'); // Se il nodo esiste, ne imposto l'altezza if (iframe) iframe.setStyle('height', json.height); } catch(e) { console.error(e); } }, false); </aui:script>

    Forte vero? Chiaramente vi ho fatto vedere solamente come modificare l'altezza ma nel messaggio potete inviare qualunque informazione e farne poi quello che volete.

  2. Eccoci di nuovo qui, con il primo post del 2020 anche se siamo già a marzo...

    Oggi vi voglio parlare di qualcosa di piuttosto semplice ma che ha richiesto il solito tempo di indagine nei sorgenti di Liferay per capire se e come era possibile fare quanto richiesto.

    A partire dalla versione 7, Liferay consente di graficare le checkbox anche in un modo alternativo molto più accattivamente; in pratica anzichè dichiarare il campo booleano nel seguente solito modo (ipotizzando che male sia un campo booleano della nostra entità):

    <aui:input name="male" />

    possiamo definirlo anche così:

    <aui:input checked="<%= user.isMale() %>" name="male" type="toggle-switch" />

    Questo farà sì che a video, al posto della solita noiosa e bruttina checkbox, sia visualizzato un componente grafico molto più gradevole e moderno.

    Il problema di questa soluzione è che il componente grafico visualizza automaticamente l'etichetta SI oppure NO a seconda del valore del campo booleano; apparentemente non è possibile modificare queste etichette perchè il DTD della taglib non lo consente.

    Tuttavia, spulciando tra i sorgenti del portale, ho scoperto che la taglib supporta i dynamic attributes ossia, in parole povere, consente di specificare attributi non definiti nel DTD; guarda caso, tra i vari attributi, ci sono anche quelli per personalizzare le suddette etichette: labelOff e labelOn. Ecco quindi come definire il campo del form:

    <aui:input checked="<%= user.isMale() %>" labelOff="female" labelOn="male" name="male" type="toggle-switch" />

    Vi lascio come esercizio quello di scoprire a cosa servono gli altri attributi dinamici supportati dalla taglib:

    • autoComplete
    • buttonIconOff
    • buttonIconOn
    • dayLight
    • displayStyle
    • format
    • iconOff
    • iconOn
    • nullable
    • timeFormat

    Enjoy!

  3. Ciao a tutti e bentrovati sul nostro blog!

    Oggi pomeriggio, mentre m'annoiavo, ho deciso di risolvere uno dei puzzle più grandi che m'attanagliano da sempre su Liferay: capire dove finiscono (o dove hanno infilato.. :)) le JSP che sono richiamate dalle taglib di portale!

    [NdJ: lo so, non è una di quell cose da non doverci dormir di notte, però sapete che non ho una vita semplice, quindi abbiate pietà di me! ;D]

    Prima di svelarvi il segreto, però, vorrei farvi comprendere il caso d'uso, così che, se dovesse capitare anche a voi, possiate arrivare celermente ad una soluzione!

    Taglib core di prodotto e JSP che ne implementano la view

    Una volta mi sono trovato a dover mettere mano all'output di una taglib e, di conseguenza, mi sono fiondato nella classe java ce ne implementava la logica e sono andato a prendermi il nome della pagina JSP corrispondente per modificarla.

    Ora non ricordo più quale fosse quella volta la taglib, però, per semplicità, prendiamo come esempio la com.liferay.taglib.ui.IconDeleteTag.

    Se aprite questa classe e andate a cercare la JSP, troverete questo statement:

    private static final String _PAGE = "/html/taglib/ui/icon_delete/page.jsp";

     

    Allora, come spesso faccio quando parto per la tangente ;), sono partito a cercare sul FS il file incriminato ma...

    jedd@ne:~/liferay-ce-portal-src-7.1.1-ga2$ find . -type f -name page.jsp | grep -i icon_delete

    jedd@ne:~/liferay-ce-portal-src-7.1.1-ga2$

     

    Niente! Della mia JSP non c'è traccia! :(

    Allora, sconfortato e sempre pronto a reinventar la ruota come se non ci fosse un domani, sarò sicuramente partito a creare la mia versione di taglib per sistemare il problema che avevo (non ricordo, ma conoscendo i tempi che ho a disposizione io quando faccio questo genere di cose, può essere che non mi sbagli poi di tantissimo.. :)).

    E invece, come sempre, bastava leggere! :)

    Se io avessi investito più di 7 secondi per analizzare il codice della taglib, forse (dico forse perché non è detto.. :D) mi sarei accorto che all'inizio del metodogetPage() c'è questaimplementazione:

    if (FileAvailabilityUtil.isAvailable(servletContext, _PAGE)) {
        return _PAGE;
    }

     

    la quale mi avrebbe fornito un indizio molto interessante, ovvero che il concetto "IF IS AVAILABLE"può significare, per logica ;), che il file potrebbe anche non esserci.. Ah, ecco perché non lo trovavo.. :D

    Allora da dove viene preso il codice che genera l'output della taglib?

    Chiaramente il codice è presente nel resto dell'implementazione del metodo getPage(); ora però c'è da capire il perché e, soprattutto, come dovrei fare io se volessi modificarne l'output.

    Perché l'output della taglib è dentro al metodo getPage()?

    Chiaramente, al solito, i nostri amici hanno ottimizzato l'implementazione per far si che l'overhead di rendition delle singole taglib -che, come avrete notato anche voi in LR sono usate parecchio.. :D- sia super ridotto! A tal fine, quindi, hanno semplicemente eliminato le singole page.jsp e portato il codice che esse contenevano all'interno del metodo getPage().

    Come faccio ad affermare che il motivo è questo? :)

    Beh, continuate a leggere.. :D

    Ok ma.. Come faccio a modificare l'output di una taglib di LR?

    Questa cosa l'ho scoperta, invece, navigando sul FS e cianfrusagliando tra i sorgenti del portale..

    Ho trovato dentro le taglib dei file chiamati page.jsp.readme che, stranamente :D, ho aperto per leggerli..

    Guardate che cosa ci ho trovato dentro:

     The content of this JSP file was inlined to improve performance. To use your own content instead, remove the ".readme" extension and add your content to this file.

     

    Eggià: avevo proprio ragione!

    I file sono scomparsi perché hanno integrato la logica nel getPage() e se volessi modificarne l'output mi basterebbe seguire il suggerimento!

    Molto bene! A questo punto, però, ripenso al comando che ho dato prima.. E lo rilancio aggiustandolo:

    jedd@ne:~/liferay-ce-portal-src-7.1.1-ga2$ find . -type f -name page.jsp.readme | grep -i icon_delete

    jedd@ne:~/liferay-ce-portal-src-7.1.1-ga2$

     

    Ma come?? Non c'è il mio file!!

    Quindi?? Come faccio a modificare il mio output?

    Beh, questa volta il gioco è semplice: siccome il mio getPage() s'aspetta che la JSP possa esserci, dovrò solo fare in modo di fargliela trovare nel punto giusto del FS!

    E per farlo, mi basterà fare un fragment host per fargliela trovare! :)

    Anche per oggi è tutto; spero d'aver chiarito anche a tutti voi uno dei misteri più grandi di Liferay che da anni m'attanaglia la vita.. 

    ... E concludo dicendo che la seniority, se proprio vogliamo, sta anche nel saper leggere il codiceCosa che, evidentemente, come dice anche l'Ing. Napolitano :D, io ancora non ho imparato a fare!

    Passate un buon fine settimana e divertitevi (magari non con i sorgenti di LR, se potete! :D), voi che potete!

    Alla prossima!

  4. Nella vostra esperienza con Liferay vi potreste trovare ad aver bisogno di dover rimpiazzare o estendere @Component contenuti all'interno di package di Liferay internal. Quando i package sono "internal" non sono esportati pertanto anche dichiarando la dipendenza dal package nel file build.gradle del vostro modulo, questa, nella migliore delle ipotesi, non darà errori a tempo di compilazione ma produrrà un errore a runtime per dipendenza non risolta.

    Vediamo come si può ovviare a questo problema basandoci sull'esempio di una implementazione che ho dovuto realizzare.

    L'implementazione consisteva essenzialmente nel realizzare una ricerca customizzata a livello di condizioni di ricerca e visualizzazione dei risultati prodotti  appoggiandosi il più possibile alla ricerca nativa di Liferay. La versione di Liferay utilizzata è la 7.1.3.

    La mia ricerca doveva essere effettuata solo per contenuti di tipo "KBArticle". In certe condizioni che non specificherò perchè non inerenti alla soluzione della problematica, quello che succedeva era che nell'operazione di costruzione delle query per ElasticSearch, venissero create condizioni basate su entità diverse dalla "KBArticle", ad esempio Wiki, JournalArticle etc, e perciò non di nostro interesse ai fini della produzione dei risultati custom corretti.

    Avevo perciò la necessità di intervenire a livello del component:

     @Component(immediate = true, service = PreFilterContributorHelper.class) public class PreFilterContributorHelperImpl implements PreFilterContributorHelper {
     
    per limitare la creazione delle query di prefiltraggio alla sola entità "KBArticle".
     
    La classe PerFilterContributorHelperImpl è però contenuta all'interno del package com.liferay.portal.search.internal.indexernel jar "com.liferay.portal.search-5.0.26.jar"che come dice il nome è un package internal quindi non esportato.
     
    Esiste uno stratagemma per esportare un package internal. Vediamo come fare:
    ho creato un bundle Liferay Module Project Fragment che ho chiamato portal-search-filter.
     
    Nel file bnd.bnd del modulo portal-search-filter ho esportato il package di mio interesse "com.liferay.portal.search.internal.indexer" in questo modo:
     
     Bundle-Name: portal-search-filter Bundle-SymbolicName: portal.search.filter Bundle-Version: 1.0.0 Fragment-Host: com.liferay.portal.search;bundle-version="5.0.26" Export-Package: com.liferay.portal.search.internal.indexer
    Attenzione che nel frammento non ho selezionato di fare l' "override" di nessun file .jsp in quanto l'unico e solo scopo del frammento è quello di esportare un package di Liferay di tipo internal. 
     
    A questo punto ho potuto creare un mio @Component custom che estende la classe PreFilterContributorHelperImpl con un service.ranking più elevato del service di default in modo da mandarlo in esecuzione. Così:
     
     @Component(immediate = true, service = PreFilterContributorHelper.class, property = { "service.ranking:Integer=1000" }) public class PreFilterContributorHelperCustom extends PreFilterContributorHelperImpl implements PreFilterContributorHelper
     
    Nella classe PreFilterContributorHelperCustomho potuto fare i seguenti import:
     
     import com.liferay.portal.search.internal.indexer.PreFilterContributorHelper; import com.liferay.portal.search.internal.indexer.PreFilterContributorHelperImpl;
     
    package che sono esportarti dal Fragment portal-search-filter che ho creato allo scopo in precedenza.
     
    A questo punto ho raggiunto il mio scopo, posso tranquillamente fare l'override dei metodi di PreFilterContributorHelperImpl e variare il comportamento della mia classe perchè crei query di filtraggio su ElasticSearch sulla sola entity KBArticle.
     
     
  5. Buongiorno a tutti e ben tornati alle cronache di Jader e dei suoi animali mitologici! ;)

    Questa mattina vi racconterò una nuova puntata della saga, oramai divenuta celeberrima :), sugli animali mitologici e i loro casi d'uso un po' fuori dall'ordinario!

    In realtà, ad essere del tutto onesti, qui il caso d'uso non m'è stato imposto da loro direttamente, però diciamo che la colpa rimane loro perché.. Sono loro che hanno scelto lo strumento del male per il quale ho dovuto scomodare dei nuovi amici!

    Ma entriamo subito nel vivo, così che anche voi possiate beneficiare di questa funzionalità di Liferay!

    Questa volta però, prima del caso d'uso, devo darvi un po' di contesto!

    RTIR: il sistema di ticket da interfacciare!

    In queste settimane mi è toccata la parte più noiosa del -chiamiamolo così..- lavoro: ho dovuto fare un refactor (nemmeno fossi l'Ingegner Napolitano quando apre il mio codice! :D) di un client REST per interfacciare RTIR: un sistema di ticket molto gradito al Cliente.

    RTIR, per chi non lo sapesse è una nuovissima e fiammante web interface scritta nientemeno che in perl (si, nel 2019 ancora il perl..) di cui hanno fatto ben 2 release di API REST. [NdJ: "nuova e fiammante", onestamente, è una battuta...]

    Oddio.. In tutta onestà chiamarle "API REST" è forse un po' troppo: come potrete vedere dall'abbondante e copiosa documentazione (versione 1.0 e versione 2.0), tutto sono tranne che API disegnate come si deve! 

    Diciamo che quando hanno fatto quella roba lì (insieme inclusivo anche della pagina singola di documentazione per ogni release dell'API..), forse non avevano bene in mente che qualcuno avrebbe dovuto un giorno invocarle davvero.. 

    Tuttavia, siccome m'è toccato, come si dice spesso ;), mi sono messo di buona lena e ho interfacciato queste API facendo quello che andava fatto (non lato codice, lato pratico: mi sono tappato il naso e mi sono messo a fare senza rompere -troppo- le scatole! :D).

    Cookie, autenticazioni, bearer token, trust manager.. Nient'altro?? :D

    Senza perdermi oltre nel criticare un sistema che sicuramente sarà fichissimo (tanto da essere stato scelto dal Cliente dei miei animali mitologici come sistema di ticket interno), mi sono lanciato nel disegnare tutti gli aspetti della nostra interfaccia, considerando chiaramente anche gli aspetti di configurazione.

    Nel farlo, ho scoperto che mi sarebbero serviti diversi flag (con relativi parametri):

    • autenticazione basata su Basic Auth: vuoi non proteggerlo dal web?
    • autenticazione basata su bearer token: vuoi non metterlo, in una API REST?
    • autenticazione basata su user / pass: eggià, nel 2019 questo è uno dei metodi consigliati... 
    • utilizzo di un trust manager custom: vuoi non installare un certificato self signed, quando uno vero ormai te lo danno gratis?

    Chiaramente, nel mio modello, più di uno di questi meccanismi doveva essere implementato pena il non funzionamento dell'intero cinema!

    E mentre facevo tutto questo, sul quale tralascio ovviamente la parte di codice perché non funzionale a questo post, mi rendevo conto che avrei avuto un leggerissimo problema di performance: per come stavo strutturando il client (legato per indirettezza a come sono disegnate le API...), avrei avuto metodi stateless che, a singola invocazione, avrebbero dovuto invocare due URL per singola chiamata.

    Questo meccanismo "a doppia chiamata" si rendeva necessario proprio per i flag di cui sopra: la prima chiamata sarebbe servita per ottenere l'autenticazione (cookie, token, quel che l'è), la seconda per invocare effettivamente l'API.

    Peccato che il grafo da navigare di questo coso sia oscenamente grosso: inutile dire che, a fronte del caricamento del dettaglio di un ticket, mi trovo a dover invocare 6 / 7 URL che, moltiplicate per la fase di autenticazione, diventano 12 / 14 URL..

    Faccio presente che questo perverso meccanismo a multi chiamata per avere il dettaglio di un ticket è normato dal design della API di cui sopra, quindi non sono io che non ho idea di come fare un client REST, sono loro che non hanno idea di come disegnare una API.. :/

    Avendo costruito però tutto il client REST (ed essendo anche soddisfatto del mio lavoro, nonostante le critiche che l'Ingegner Napolitano mi muoverebbe se vedesse il mio codice.. :D), mi scocciava ridisegnarlo da capo per evitare questo problema..

    E allora mi sono detto: "visto il codice sul quale ho fatto refactor, nessun s'accorgerà della melma che ho disegnato e forse la passerò liscia.." ;D

    Ma poi, più lo utilizzavo per farci i test e più mi rendevo conto che no, dai, era davvero osceno: sia in termini di performance sia in termini di design.. Per quanto i miei animali mitologici mi torturino, non mi sembrava il caso di lasciare loro del codice fatto così ammmelma!

    Però la pigrizia la faceva da padrona..

    Siccome lo statement qui sopra è sempre vero ;), avendo io disegnato un client che fa esattamente tutto quello che mi serviva e non volendolo ridisegnare completamente ;), mi sono interrogato su come rendere più performante il mio codice!

    Chiaramente, evitare il throttling delle URL sarebbe la prima cosa da fare; però come avrei potuto farlo se tutte le URL da invocare fanno riferimento a pojo differenti e devono essere riutilizzabili anche in metodi differenti? Inoltre, come potevo rendere la API indipendente dall'implementazione fisica del client REST senza necessariamente dover creare un accoppiamento con questo nelle firme?

    Ed è stato proprio qui, mentre pensavo a tutte queste cose, che m'è venuto in mente un post di Daniele Catellani sul fascino dei thread local, fatto davvero tantissimi anni fa!

    Però poi mi sono anche ricordato dello sbatti mostruoso che bisogna fare per usarli (non in termini di codice, ma in termini di gestione) [NdJ: se non avete idea di cosa io sto dicendo ;), questo articolo di LR fa al caso vostro..).

    E allora, come al solito ;), mi sono messo a scavare.. :D

    A questo punto già avrete intuito cosa mi sono messo a fare! Come spesso accade lavorando con Liferay ;), in più di un milione di righe di codice, sicuramente esiste qualche cosa che fa già quello che mi serve.. :)

    Quindi, anziché partire in quarta a inventare il meccanismo definitivo custom per la gestione dei thread local, avendone visti implementati in LR a bizzeffe :D, mi sono messo a cercare e ho trovato i miei nuovi amici: i CentralizedThreadLocal! :)

    CentralizedThreadLocal: chi sono e cosa fanno!

    Questa classe è stata progettata per risolvere in maniera molto semplice ed elegante una serie di problemi che esistono nella gestione dei thread local. Ad esempio, permette di ripulire il, chiamiamolo così..., singleton del thread local specifico per ogni request, evitandovi lo sbatti di doverlo fare voi.

    Chiaramente, per farlo usa sempre i "soliti" meccanismi: c'è un simpatico filtro che si occupa di ripulire il thread local dopo ogni invocazione, così da non doverci caricare dell'onere di farlo.

    A questo punto, però, dobbiamo vedere insieme un po' di codice, per capire nel dettaglio come implementarne uno e come fare in modo che tutti i nostri problemi, magicamente, si dissolvano! :D

    Piccola premessa: la classe CentralizedThreadLocal si trova all'interno di due package di prodotto; la prima, il kernel, dove questa è deprecata e la seconda, dentro a petra.lang, che è quella che dobbiamo utilizzare!

    package it.dvel.rtir.client.util;

    import aQute.bnd.annotation.ProviderType;
    import com.liferay.petra.lang.CentralizedThreadLocal;

    import javax.ws.rs.core.NewCookie;
    import java.util.List;

    @ProviderType
    public class RtirCookieThreadLocal {

        public static List<NewCookie> getCookies() {
            return _newCookies.get();
        }

        public static void setCookies(List<NewCookie> cookies) {
            _newCookies.set(cookies);
        }

        private static final ThreadLocal<List<NewCookie>> _newCookies =
                new CentralizedThreadLocal<>(
                        RtirCookieThreadLocal.class + "._newCookies",
                        () -> null);


    }

    Come potete notare, la classe è molto stupida: di fatto è un wrapper sull'oggetto ThreadLocal che però viene valorizzato con una istanza di CentralizedThreadLocal.

    A questa istanza vengono passati due dettagli importanti: come si chiama il ThreadLocal, all'interno della mappa che il padre di tutti questi oggetti si crea (quindi questo nome dev'essere univoco perché interpretato come un id..) e una lamda che fornisce un'implementazione della @FunctionalInterface Supplier, ovvero del wrapper per la costruzione del valore di default del nostro ThreadLocal.

    Di default, usando il costruttore che abbiamo utilizzato noi, viene anche impostato internamente al ThreadLocal il flag shortLived a true, il che significa che il nostro oggetto sarà ripulito "al termine di ogni esecuzione della richiesta".

    Ed ecco ottenuto il nostro ThreadLocal nuovo fiammante e assolutamente in grado di risolvere -elegantemente, aggiungerei ;)- anche il problema della condivisione all'interno del nostro thread delle informazioni necessarie a non dover riautenticare il Client ad ogni richiesta!

    Ma le magie del CentralizedThreadLocal non finiscono qui!

    Se andate a vedere nel codice sorgente quali altre opportunità vi offre :), scoprirete una serie di feature davvero interessanti..

    .. Però qui io mi fermo!

    Adesso è il vostro turno: aprite il codice e divertitevi a scoprire quali e quante cose può fare per voi il nostro nuovo amico e, come al solito ;), se volete potete commentare in questo post le vostre scoperte, così da renderle utili a tutti!!!

    Anche per oggi è tutto, buon divertimento con i CentralizedThreadLocal, Liferay e.. I vostri animali mitologici!! :)

    A presto, ciao, J.