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. 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.
     
     
  2. 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.

  3. Buongiorno a tutti e ben ritrovati con "Le cronache di Jader", appunti -quasi- sensati per sopravvivere al difficile mondo dell'IT su Liferay!

    Oggi vi parlerò di un problema che i miei animali mitologici mi hanno posto la scorsa settimana: mantenere sincronizzati i permessi tra due modelli differenti!

    Come al solito, partirò raccontandovi il perché di questo requisito con un caso d'uso che spero sia sensato, così che possiate anche voi trarne spunto e capire velocemente se fa al caso vostro (non il caso d'uso, la soluzione! :)).

    Mantenere sincronizzati i permessi di due modelli differenti: caso d'uso

    Il caso d'uso è semplice: ho due oggetti (nel mio caso un oggetto custom che chiameremo, con enorme fantasia ;), ModelloCustom che al suo interno ha una referenza ad un DlFileEntry.

    Il caso d'uso vuole che, a prescindere da quale delle due entità io parta a modificare i permessi, i permessi dell'altra entità siano mantenuti sincroni.

    Quindi, per spiegarmi più semplicemente, se io modifico i permessi su ModelloCustom aggiungendo, che ne so, il permesso di VIEW e il permesso di UPDATE a uno specifico ruolo, coerentemente con questo aggiornamento mi troverò modificati gli stessi due permessi sullo stesso ruolo dell'entità DlFileEntry.

    Cose da NON fare per gestire questo caso d'uso!

    Prima di dirvi come fare a gestire questo caso :), vi dico quello chenon dovete fare per gestirlo! :)

    Intendo che si, anche io ci ho messo un po' per risolverlo e, nel mio "tentare", ho provato diverse strade per riuscirci.

    Abusare dei value object listener: cattiva idea.. :/

    La prima strada che ho percorso era quella dei value ojbect listener: ho creato dei listener (che dovrebbero servire a gestire l'integrità referenziale ma dei quali io ho abusato per i miei scopi.. ;)), uno per ModelloCustom e uno per DlFileEntry intercettando la modifica all'evento relativo sull'entità ResourcePermission.

    Sembrava funzionare ;), però poi mi sono accorto che gli update finivano per "rompere" i permessi; nel senso: le update venivano propagate correttamente ma, non so il perché :), quando il pannellino dei permessi si ricaricava questo mostrava permessi diversi da quelli che avevo impostato.

    Sicuramente i miei animali mitologici si sarebbero lamentati di questo comportamento un po' bizzarro ;), quindi ho scartato questa implementazione e ho proseguito nel mio scouting..

    Service Wrapper: panacea di tutti i miei mali?

    Ovviamente la risposta è.. NO!

    Posto che non ho capito perché ma la 7.1.2 non mi caricava il ServiceWrapper che avevo creato (o meglio: caricare lo caricava, però non lo invocava e questa cosa mi aveva un po' tediato..) ho pensato che comunque non sarebbe stata una gran implementazione: il service della ResourcePermission espone, #chevelodicoafare :), mille milioni di metodi e scoprire quali fossero quelli giusti mi sembrava già di per se troppo sbatti.. :)

    Forte del fatto poi che non riuscivo a far invocare il mio ServiceWrapper mi sono arreso in -credo- 8 minuti scartando anche questa strada!

    E allora.. Custom sia! :)

    Confrontandomi con l'animale mitologico che nel frattempo stava umoralmente cambiando il suo mood nei confronti della cosa, sentendolo sempre più disperato e triste (e con l'avvicinarsi del terribile momento del Collaudo col Cliente -fase della vita di un (quasi) dev che mette sempre una certa adrenalina, ammettiamolo! :D) mi sono detto "beh, sai che c'è? Che se il pannellino maledetto standard non fa quello che voglio io, allora me ne faccio uno custom dove piloto quello che mi pare sui permessi e amici come prima!".

    L'idea, parlandone anche con il mio animale mitologico, sembrava addirittura sensata! Ci siamo lasciati alla fine della telefonata super convinti che questa fosse LA strada!

    ... E invece no!

    Questa cosa del custom a tutti i costi non è mai una buona idea, soprattutto quando lavori con un mastodonte come Liferay!

    E quindi? Che ho fatto?

    Beh, premetto che.. A posteriori "la cosa del custom a tutti i costi non è mai una buona idea", ma sul momento.. Ero partito per implementarla! :)

    E siccome sono uno pigro mi son detto "non è che devo reinventare la ruota: guardiamo come fa Liferay a gestire la costruzione del pannellino e come legge poi i parametri dalla matrice Azioni / Ruoli che disegna e replico tutto nel mio pannellino custom!".

    Mamma mia quanto sono pigro e furbo.. :D

    Per fortuna so ancora leggere il codice (anche se faccio queste cose tipo alle 4 della mattina, ma vabbé..) e, facendo scouting del codice della com.liferay.portlet.configuration.web.internal.portlet.PortletConfigurationPortlet e più precisamente il suo simpa metodo updateRolePermissions ho conosciuto dei nuovi amici.. :)

    PermissionPropagator: ecco la risposta che cercavo! ;)

    Spoiler ON: faccio presente che l'obiettivo funzionale per il quale i PermissionPropagator sono stati inventati NON è quello al quale ambisco io. In realtà questi cosi sono stati progettati per Propagare sulle entità figlie i permessi (ad esempio se cambio il permesso di visibilità su un thread del forum e vorrei che tutte le risposte al suo interno fossero automaticamente allineate senza doverle modificare a mano..).

    Però è anche vero che, alla fin fine, il mio obiettivo è simile: anche io vorrei che si propagassero i permessi tra il mio ModelloCustom e la mia DlFileEntry!

    Quindi ho proceduto, senza indugio, a testare questa soluzione!

    Vediamo un po' di codice, per capire se questa roba ha senso anche per voi! :)

    Annotiamo la classe come Component

    @Component(
            immediate = true,
            property = {
                    "javax.portlet.name=" + PortletKeys.MODELLO_CUSTOM,
                    "javax.portlet.name=" + PortletKeys.DOCUMENT_LIBRARY_ADMIN
            },
            service = PermissionPropagator.class
    )

    Siccome io volevo bidirezionalmente aggiornare i permessi, quindi sia che si partisse dai permessi di ModelloCustom sia che si partisse dai permessi del DlFileEntry, ho mappato il mio PermissionPropagator su entrambe le portlet di gestione!

    Come sempre esiste una Base.. :)

    public class MyPermissionPropagator extends BasePermissionPropagator {

    ...

    }

    Anche in questo caso ho esteso la classe base che Liferay mette sempre a disposizione...

    Implementiamo il metodo richiesto dall'interfaccia!

    @Override
        public void propagateRolePermissions(ActionRequest actionRequest,
    String className, String primKey, long[] roleIds) throws PortalException {        
            if (ModelloCustom.class.getName().equals(className)) {
                propagateRolePermissionsToFileEntry(actionRequest, className, primKey, roleIds);
            } else if (DLFileEntry.class.getName().equals(className)) {
                propagateRolePermissionsToModelloCustom(actionRequest, className, primKey, roleIds);
            }
        }

    Ho poi implementato il metodo che l'interfaccia PermissionPropagator richiede per funzionare!

    Ho sfruttato le facility che la Base mi mette a disposizione! ;)

    private void propagateRolePermissionsToModelloCustom(ActionRequest actionRequest,
    String className, String primKey, long[] roleIds) throws PortalException {
            long fileEntryId = GetterUtil.getLong(primKey);
            ModelloCustom modelloCustom = modelloCustomLocalService.fetchByFileEntryId(fileEntryId);
            if (Validator.isNotNull(modelloCustom)) {
                for (long roleId : roleIds) {
                    propagateRolePermissions(actionRequest, roleId, DLFileEntry.class.getName(),
    fileEntryId, ModelloCustom.class.getName(), modelloCustom.getPrimaryKey());
                }
            }
        }

        public void propagateRolePermissionsToFileEntry(ActionRequest actionRequest,
    String className, String primKey, long[] roleIds) throws PortalException {
            ModelloCustom modelloCustom = modelloCustomLocalService.getModelloCustom(GetterUtil.getLong(primKey));
            if (modelloCustom.getFileEntry() > 0) {
                for (long roleId : roleIds) {
                    propagateRolePermissions(actionRequest, roleId, ModelloCustom.class.getName(),
    modelloCustom.getPrimaryKey(), DLFileEntry.class.getName(), modelloCustom.getFileEntry());
                }
            }
        }

    È qui che accade la magia! :)

    La chiamata alla propagateRolePermissions è chiaramente quella che sfrutto per far accadere la magia e, nemmeno a dirlo, è quella messa a disposizione dalla mia Base!

    Cosa fa di magico questo metodo?

    In pratica fa una cosa molto.. Pratica! :)

    Si preoccupa di capire quali sono i permessi in comune tra le due entità (mappati nella tabella ResourceAction) e poi li sincronizza! Tuto gratis, tutto trasparente e, soprattutto, tutto.. Che funziona! :)

    Beh, anche per oggi abbiamo imparato qualche cosa di nuovo (cfr. "smettila di pensare che il custom sia l'unica via per perseguire l'obiettivo: su più di 1.000.000 di righe di codice ci sarà qualche cosa che fa già al caso tuo, no?:)") e siamo pronti a propagare permessi come se non ci fosse un domani! ;)

    Alla prossima e divertitevi con Liferay 7.x, OSGi e... I vostri animali mitologici!! :)

    A presto, ciao, J.

    P.S. Stavo quasi per dimenticarmi!

    Aggiungete questa riga nel vostro portal-ext.properties altrimenti non funzionerà mai l'implementazione che avete appena fatto!

    ##
    ## Permissions
    ##
        #
        # Set the following to true to enable propagation of permissions between
        # models.
        #
        # For example, when setting the permissions on a specific Wiki node, if you
        # assign a role a permission (e.g. DELETE), then the assignment of that
        # permission is also propagated to all Wiki pages that belong to that Wiki
        # node.
        #
        # The actual logic of how permissions are propagated among models is
        # specified per portlet. See liferay-portlet.xml's use of the element
        # "permission-propagator".
        #
        # Env: LIFERAY_PERMISSIONS_PERIOD_PROPAGATION_PERIOD_ENABLED
        #
        permissions.propagation.enabled=true

     

  4.  

    Quando si desidera visualizzare un contenuto HTML all'interno di un report PDF, TIBCO JasperSoft Studio

    offre due possibilità:

    1- impostare Markup/HTML in fase di design del template .jrxml;

    2- utilizzare HTMLComponent.

    La soluzione 1, non richiede di importare librerie aggiuntive e quindi è molto veloce,ma l'insieme dei tag

    HTML che  vengono convertiti correttamente in PDF è molto ridotto,ed in particolare tag di usomolto

    comunecome <strong>, <style>, <em> e <img> vengono ignorati.

    La soluzione 2 è un poco più articolata, ma offre l'indubbio vantaggio di convertire correttamente un

    numeromaggioredi tag HTML.

    HTMLComponent è una libreria prodotta da JasperSoft, che è stata ed è ancora ampiamente utilizzata,

    cometestimoniano i numerosi thread di discussione reperibili online.

    Al momento, però, non viene distribuita dai repository Maven per il download e non è disponibile sulla

    palettedeglistrumenti di TIBCO JasperSoft Studio 6.3.1.final.

    Tuttavia JasperSoft Studio deve continuare a gestirla per ragioni di retrocompatibilità, e quindi, di fatto

    quandonel template .jrxml trova questo codice:

            <bandheight="742">
            <componentElement>
                <reportElementx="0"y="0"width="555"height="742"/>
                <hc:htmlxmlns:hc="http://jasperreports.sourceforge.net/htmlcomponent"

    xsi:schemaLocation="http://jasperreports.sourceforge.net/htmlcomponent

    http://jasperreports.sourceforge.net/xsd/htmlcomponent.xsd"

    scaleType="RetainShape"horizontalAlign="Left"verticalAlign="Top">
                    <hc:htmlContentExpression><![CDATA[$P{myHtmlContent}]]></hc:htmlContentExpression>
                </hc:html>
            </componentElement>
        </band>

    JasperSoft Studio è in grado di mostrare tutte le properties dell 'HTMLComponent in formato visuale e

    consente di modificarle in modo assistito.

    In aggiunta, le versioni più recenti di JasperSoft Studio, come la 6.3.1 in questione, consentono di utilizzare

    HTMLComponent all'interno del tool di palette 'Generic Element' indicando i seguenti valori.

    Generic Type Name:htmlelement
    Generic Type Namespace:http://jasperreports.sourceforge.net/jasperreports/html

    In questo modo tutte le proprietà e i parametri possono essere visualizzate all'interno del pannello di

    propertiesdel 'Generic Element',anche se la loro modifica è meno assistita che nel caso precedente,

    trattandosi di un tool generico.

    Il risultato del nostro operato sarà, ad esempio, il seguente frammento sul template .jrxml.

               <bandheight="742">
           <genericElement><reportElementx="0"y="0"width="555"height="742"/>
                <genericElementTypenamespace="http://jasperreports.sourceforge.net/jasperreports/html"

    name="htmlelement"/>
                <genericElementParametername="htmlContent">
                    <valueExpression><![CDATA[$P{myHtmlContent}]]></valueExpression>
                </genericElementParameter>
                <genericElementParametername="scaleType">
                    <valueExpression><![CDATA["RetainShape"]]></valueExpression>
                </genericElementParameter>
                <genericElementParametername="verticalAlign">
                    <valueExpression><![CDATA["Top"]]></valueExpression>
                </genericElementParameter>
                <genericElementParametername="horizontalAlign">
                    <valueExpression><![CDATA["Left"]]></valueExpression>
                </genericElementParameter>
            </genericElement>
        </band>
     

    Comunque la si utilizzi, la libreria HTMLComponent, necessita al momento di essere configurata su Liferay

    mediante i seguenti passaggi.

    Queste istruzioni si riferiscono al progetto in cui è già installato e funzionante JasperReport, cioè in cui sono

    state dichiarate le dipendenze dalle librerie di JasperReport.

    1- Copiare nella cartellasrc/main/resources/lib tutti i jar contenuti in nella cartella 

       Jaspersoft Studio-6.3.1.final\configuration\org.eclipse.osgi\38\0\.cp\lib

    2- Aggiungere al build.gradle le dipendenze

       dependencies {

                      compileInclude  name: 'htmlcomponent'
                      compileInclude  name: 'core-renderer'
                      compileInclude  name: 'jtidy-r938'

                     //oppure, se si preferisce, 'jtidy' può essere scaricato dai repository Maven:

                    //compileInclude group: 'net.sf.jtidy', name: 'jtidy', version: 'r938'

       }

    3- Indicare sul build.gradle la locazione in cui si trovano queste librerie

       repositories {
           flatDir {
                 dirs 'src/main/resources/lib'
           }
       }
     
     

    4- Aggiungere queste esclusioni al file bnd.bnd

        !org.xhtmlrenderer.test,\
        !org.xhtmlrenderer.test.*,\

    A questo punto si è pronti per produrre i PDF partendo da un contenuto HTML.

    Va ricordato che tutto il contenuto HTML viene trasformato da questo componente in un' unica immagine,

    che pertanto potrebbe essere visualizzata diversamente rispetto ad un browser HTML e a seconda del

    formato di esportazione utilizzato.

     

     

     

     

     

  5. 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...