I cookie ci aiutano a fornire i nostri servizi.

Utilizzando tali servizi, accetti l'utilizzo dei cookie da parte nostra. Per saperne di piu'

Approvo

D'vel Blog

A partire dalla fine del 2010 lavoro come Liferay Expert presso l'azienda D'vel snc, per la quale scrivo spesso articoli tecnici sul blog.

Il blog è davvero molto utile e ricco di preziosi consigli; per chi fosse interessato l'indirizzo è il seguente: blog.d-vel.com.

Ecco un estratto degli ultimi articoli scritti.

blogdvel
  1. Qualche anno fa avevo affrontato lo stesso tema relativamente alla neonata versione 7.0; per chi se lo fosse perso vi ripropongo il link: https://blog.d-vel.com/home/-/blogs/collegarsi-a-datasource-esterni-con-liferay-dxp.

    Con la versione 7.1 ci fu un piccolissimo cambiamento (che potete trovare nei commenti del vecchio articolo) ma con l'avvento della versione 7.2 (e successive) tutto è cambiato! Di nuovo...

    Vediamo quindi come fare per configurare adeguatamente la connessione ad un database esterno con Liferay 7.2+ (metto il + perché sono ottimista e spero di non dover scrivere un altro articolo tra qualche anno).

    Resta però un punto fermo per tutte le versioni di Liferay 7+: tutte le entità definite all'interno del medesimo service.xml si collegano al medesimo datasource pertanto, per ogni datasource esterno a cui collegarsi, andrà definito uno specifico plugin di tipo Service Builder. Ma in fondo questa cosa rende il codice molto più modulare e manutenibile.

    La prima modifica da fare è quella di specificare per tutte le entità del service.xml l'attributo data-source andando ad inserire un qualsiasi valore fittizio, ad esempio:

     <entity data-source="extDataSource" name="Foo"> [...] </entity>

    Ovviamente, se necessario, andate a specificare anche gli attributi table e column (che mi aspetto sappiate cosa sono ed a cosa servono).

    Cosa scrivete nell'attributo data-source non ha molta importanza perché non viene usato da nessuna parte, serve solamente per comunicare a Liferay (ed al Service Builder) che non bisogna usare la connessione di default del portale.

    Dopodiché andiamo a definire nel portal-ext.properties i parametri di connessione al database esterno:

     jdbc.ext.driverClassName=<DRIVER_CLASS_NAME> jdbc.ext.url=<CONNECTION_URL> jdbc.ext.username=<USERNAME> jdbc.ext.password=<PASSWORD> jdbc.ext.connectionTestQuery=<CONNECTION_TEST_QUERY> (se serve)

    La cosa importante da ricordare è quella di utilizzare un prefisso univoco per ciascuna connessione esterna (nel caso ne aveste più di una); nel nostro caso il prefisso che abbiamo utilizzato è jdbc.ext.

    Bene, fino a questo punto le modifiche da fare sono esattamente uguali a quanto si faceva nelle versioni precedenti di Liferay. Quello che è cambiato arriva adesso!

    Infatti a partire dalla versione 7.2 è necessario implementare un'apposita classe di tipo DataSourceProvider per comunicare a Liferay quale connessione esterna utilizzare; questa classe andrà implementata indipendentemente dal fatto che il vostro service.xml abbia impostato ds o spring come meccanismo di injection.

    Non vi sto ad annoiare con la teoria perché probabilmente avrete già perso un sacco di tempo a cercare su Google, quindi mi limito a mostrarvi il codice della classe da implementare:

     package it.dvel.liferay.service.persistence.impl.constants; import com.liferay.portal.kernel.dao.jdbc.DataSourceFactoryUtil; import com.liferay.portal.kernel.dao.jdbc.DataSourceProvider; import com.liferay.portal.kernel.util.PropsUtil; import javax.sql.DataSource; public class DataSourceProviderImpl implements DataSourceProvider { @Override public DataSource getDataSource() { try { return DataSourceFactoryUtil.initDataSource( PropsUtil.getProperties("jdbc.ext.", true)); } catch (Exception e) { throw new RuntimeException(e); } } }

    Come vedete la classe non fa altro che restituire le property di connessione contraddistinte dal prefisso che avete scelto sopra (ossia jdbc.ext.); mi raccomando fate attenzione ad inserire anche il punto finale!

    Questa classe va messa nel plugin -service in un package qualunque; di solito uso il  package .service.persistence.impl.constants perché esiste già e sono pigro. smiley

    A questo punto manca l'ultimo passaggio!

    Sempre nel plugin -service create un file di testo nella cartella src/main/resources/META-INF/services e chiamatelo esattamente com.liferay.portal.kernel.dao.jdbc.DataSourceProvider ossia come l'interfaccia che avete implementato nella classe vista sopra.

    Il contenuto di questo file di testo sarà un'unica riga costituita dal nome completo di package della classe che avete implementato, ossia:

     it.dvel.liferay.service.persistence.impl.constants.DataSourceProviderImpl

    Ora salvate tutti i file, lanciate il task build-service del Service Builde ed una volta terminato deployate i plugin.

    Se avete seguito tutte le istruzioni alla lettera, vedrete nei log di Tomcat il messaggio relativo alla connessione al database esterno.

    Enjoy!

  2. Ciao a tutti!

    L'articolo di oggi verte su uno dei pattern applicativi più noti (nella mia personale classifica, nei colloqui che facciamo è mediamente il secondo che viene detto dopo il Singleton): il factory pattern!

    Non farò una dissertazione sul pattern in se, perché credo che Google possa fornirvi tutti gli articoli dell'universo (fatti anche molto meglio di come li farei io) su di esso; tuttavia mi piaceva l'idea di darvi uno spunto per una sua possibile implementazione all'interno di un container OSGi.

    Il problema

    Come sempre, però, vi racconto il caso d'uso che mi ha portato a questa implementazione, così che anche voi possiate avere un po' di contesto.

    Il caso d'uso di questa volta, incredibile a dirsi, non arriva da uno dei miei animali mitologici, bensì da un bisogno personale!

    Mi spiego: come spesso accade, mi trovo a fare un po' di audit sul codice che ho scritto e mi rendo conto che, onestamente, un po' di refactor potrebbe migliorarne quanto meno la leggibilità se non, come in questo caso, la manutenibilità.

    Nello specifico, in un metodo, avevo il classico "if / else / if" e, dopo il terzo "if", mi sono trovato ad aggiungerne un quarto.

    Chiaramente tutti avete già storto il naso: chi non avrebbe implementato un factory già al terzo "if" alzi la mano! :)

    Io invece, siccome sono mediamente un po' più indietro della media :), ho pensato di implementarlo al quarto.

    Allora mi sono messo a ragionare (in realtà a scrivere codice mentre lo facevo.. :)), per implementarlo.

    Siccome però ero all'interno del container OSGi, oggi vero cuore pulsante di Liferay, mi sono trovato davanti a un problema: come implemento una factory che mi ritorna Service OSGi? Eggià, perché il mio caso non era banalmente sostituire la cascata di "if / else" con un factory, era anche quello di permettere agli oggetti ritornati dalla factory stessa di beneficiare della dependency injection offerta dal container.

    Allora mi sono messo di buona lena e, con un po' di buzzo buono, mi sono lanciato a fare test, leggere specifiche e, più in generale, a cercare una soluzione che risolvesse il mio problema.

    Dopo circa due ore di mal di testa (sfido chiunque a non impazzire leggendo le specifiche di OSGi riga per riga alla ricerca di un suggerimento pratico..) ho avuto un'illuminazione!

    "Ma sono un genio", ho pensato, "il service tracker è di fatto una factory (molto alla lontana ma seguite il ragionamento) e quello che ritorna, i Service OSGi, sono di fatto gli elementi che servono a me"!

    Da questa riflessione, che vi giuro mi ha spaccato in due come una mela che cade su una motosega impazzita :), ho dedotto che il Factory pattern in OSGi è morto, perché, di fatto, sostituito dal Service pattern!

    Il Factory pattern è morto: viva il Factory pattern!

    Ok, direte voi: questo è ovvio. Lo so, lo avevo anche premesso: non sono un fulmine di guerra! ;)

    Però mi sono detto: a questo punto come risolvo il problema dello "switch" che dovrei fare, per discriminare quale tra gli oggetti che sono ritornati devo usare?

    La risposta è stata ovvia: property di un Component! Posso fare uno switch in base ad una property e quindi, con un filtro, avere quello che mi serve!

    Anche qui, direte voi, ovvio. Siete un pubblico difficile quest'oggi! :)

    Ancora una volta, però, non mi sono arreso e ho cominciato a cercare una soluzione che mi mettesse felice e mi permettesse di recuperare, dato il filtro, il Service a me più congeniale.

    La soluzione

    Ed ecco la mia possibile soluzione, con la speranza che possa tornare utile anche a voi!

    Spoiler alert: quello che segue non è LA soluzione, bensì, "la migliore soluzione che ho trovato con i constraint di tempo e budget che avevo". Don't blame me for that! :)

    Ometto le parti di codice "ovvie", quelle ad esempio dove accedo al ServiceTracker, ma mi concentro sul metodo che, di fatto, rappresenta la mia factory!

     private TaskExecutor getTaskExecutor(Element element) { Class[] interfaces = element.getClass().getInterfaces(); for (Class interfaceObj : interfaces) { List executors = serviceProvider.getServices(TaskExecutor.class, "task.executor.type", interfaceObj.getSimpleName()); if (Validator.isNotNull(executors) || executors.size() > 0) return executors.get(0); } return new TaskExecutor() { @Override public boolean executeTask(ActivityTask task, ModelitDataRecord dataRecord, ModelitBPMEngineLocalService service, ServiceContext serviceContext) throws Exception { return false; } }; }

    Come potete vedere l'idea è semplice: i miei Component hanno una property (task.executor.type) sulla quale mi baso per fare il case.

    Questa property, nella mia convenzione, è mappata su un'interfaccia applicativa che gli oggetti sui quali faccio lo switch implementano. Ora, mi è ben chiaro che questa soluzione non è assolutamente scalabile, è forse un po' troppo artigianale e magari non è elegantissima; so anche che se uno dei miei oggetti implementa più interfacce (possibile), la cosa non sta in piedi ma vi garantisco che nel mio modello tutte queste cose sono superabili! :)

    Come vedete, però, l'obiettivo l'ho raggiunto: passo alla mia (finta) factory l'oggetto sul quale fare il case e a runtime, sfruttando la reflection, recupero l'oggetto a me più congeniale.

    Concludo con un dettaglio: nel mio caso è assolutamente plausibile che non ci sia un oggetto di ritorno per ogni oggetto in input, quindi ho implementato come default un DummyObject che mi consente di mantenere consistente il client e mi previene dalle NullPointerException!

    Bene: anche per oggi vi ho fatto perdere un po' di tempo ragionando su pattern e possibili implementazioni su container OSGi; come sempre, nel caso abbiate dubbi / domande, potete utilizzare il box dei commenti!

    A presto e, buon (Factory)Service pattern a tutti! :)

  3.  

    Ciao a tutti!

    Tutti sapete che la tabella Layout contiene le pagine che vengono generate dal portale; forse non tutti però conoscete i typeSettings, un campo della tabella Layout all'interno del quale, tipicamente, il portale scrive l'associazione tra gli spazi dei singoli layouttpl e le relative portlet.

    Ma come possiamo usarli "a nostro piacimento"?

    Questa domanda, come sempre, mi è stata fatta da un gruppo di ragazzi che sta facendo un ottimo lavoro mentre fanno il porting dalla 6.2 EE alle 7.2 EE di una applicazione che non hanno scritta loro ma che hanno ereditato.

    (N.d.J: Della serie: doppio carpiato rovesciato, ma bendati e legati dopo aver girato in tondo per 5 minuti e poi fatti saltare da 35 metri d'altezza... Per darvi un'idea! :))

    E la domanda, più che lecita, mi è stata rivolta perché chi ha codificato prima di loro l'applicativo, ha pensato bene di utilizzare i typeSettings anche per salvare caratteristiche delle pagine.

    Posto che noi in D'vel è una vita che facciamo queste robe (ma ce la siamo sempre spicciata facile usando i Custom Attribute :)), m'intrigava la soluzione che avevano realizzato e quindi ho lavorato con loro per riuscire a fare il porting del codice sulla 7.2.

    Il caso funzionale

    Il caso funzionale che avevano mappato era semplice: in alcune pagine è presente una portlet (scusate: widget, siamo sulla 7.. :D); questo widget però deve ereditare alcuni parametri per essere configurato, così chi ha codificato l'applicativo ha pensato bene di salvare nei typeSettings questi parametri.

    (N.d.J: faccio notare che la portlet poteva essere semplicemente configurata sulle singole pagine, senza stare tanto ad impazzire; ma non chiedetemi perché è stata scelta questa strada: se la vita fosse semplice a noi non ci cercherebbe nessuno, quindi.. ;D).

    La vecchia implementazione

    Per riuscire a fare questa implementazione, i vecchi developer avevano proceduto in questo modo:

    • avevano fatto un bell'hook sulla portlet che gestiva il back end della gestione delle pagine;
    • avevano fatto una bella JSP che si agganciava al form-navigator della gestione delle pagine;
    • accedendo alla nuova voce all'interno del form-navigator avevano messo la loro bella JSP;
    • al submit i dati venivano salvati trasparentemente nella Layout, così che il gioco fosse fatto!

    Analizzando nel dettaglio la loro implementazione, in effetti posso anche riconoscere che è sicuramente più elegante e figa della nostra:

    • il cliente non accede ai "Campi personalizzati" ma al back end standard di prodotto;
    • all'interno delle voci di configurazione della pagina c'era la loro voce;
    • accedendo alla loro voce c'era una form "impaginata a modo" (e non autogenerata come quella dei Custom Fields) che non era in effetti niente male..

    Cavolo, allora la sfida si faceva interessante! :)

    La nuova implementazione

    La prima cosa che c'era da fare, quindi, era sostituire l'hook che, sulla 6.2, si agganciava con una JSP deployata sul portale, al form-navigator presente all'interno della gestione delle pagine.

    Nella 7, il form-navigator ovviamente si è evoluto ed è quindi diventato necessario sviluppare due Components per poterlo utilizzare / per potercisi collegare in maniera trasparente.

    Il primo serve per creare la categoria all'interno del menù di navigazione del portale; il secondo per creare le singole sezioni che ci sono all'interno di questo menu.

    Quindi abbiamo proceduto in questo modo; prima abbiamo creato la categoria:

      package it.dvel.playground.web.layout; import com.liferay.portal.kernel.language.LanguageUtil; import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorCategory; import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorConstants; import java.util.Locale; import org.osgi.service.component.annotations.Component; @Component( immediate = true, property = "form.navigator.category.order:Integer=10", service = FormNavigatorCategory.class) public class CustomLayoutFormNavigatorCategory implements FormNavigatorCategory { @Override public String getFormNavigatorId() { return FormNavigatorConstants.FORM_NAVIGATOR_ID_LAYOUT; } @Override public String getKey() { // Io ho fatto una PoC; voi fate i bravi e usate una COSTANTE!! :) return "custom-category"; } @Override public String getLabel(Locale locale) { return LanguageUtil.get(locale, "custom-category"); } }

    Fatto questo, abbiamo creato il pezzo di pagina che ci interessava.. O meglio: la entry che si sarebbe agganciata alla nostra category custom:

      package it.dvel.playground.web.layout; import com.liferay.portal.kernel.language.LanguageUtil; import com.liferay.portal.kernel.model.Layout; import com.liferay.portal.kernel.servlet.taglib.ui.BaseJSPFormNavigatorEntry; import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorConstants; import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorEntry; import java.util.Locale; import javax.servlet.ServletContext; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @Component( property = "form.navigator.entry.order:Integer=100", service = FormNavigatorEntry.class) public class CustomLayoutFormNavigatorEntry extends BaseJSPFormNavigatorEntry implements FormNavigatorEntry { @Override protected String getJspPath() { return "/html/admin_layout/my_custom_fields_to_manage.jsp"; } @Override public String getCategoryKey() { // Le costanti ragazzi: voi usate le costanti! ;) return "custom-category"; } @Override public String getFormNavigatorId() { return FormNavigatorConstants.FORM_NAVIGATOR_ID_LAYOUT; } @Override public String getKey() { // Non mi stancherò mai di ripetermi: usate le costanti! ;) return "custom-entry"; } @Override public String getLabel(Locale locale) { return LanguageUtil.get(locale, getKey()); } @Override @Reference( target = "(osgi.web.symbolicname=my-poc-module-web)", unbind = "-") public void setServletContext(ServletContext servletContext) { // Questo setter è strategico: permette al container di recuperare // la JSP che abbiamo indicato sopra direttamente dal nostro bundle // e non a raglio da chissà quale pacchetto! // Ricordatevi che questa roba sarà eseguita dal bundle standard della // gestione delle pagine! super.setServletContext(servletContext); } }

    E anche questa è fatta!

    Ora non rimaneva che sistemare la nostra JSP, all'interno del nostro bundle, e far gestire tutto al componente standard di portale.

    Ed ecco qui la JSP:

     <%@ include file="/META-INF/resources/html/init.jsp"%> <% // Recupero il plid del Layout visualizzato dalla request Long selPlid = ParamUtil.getLong(renderRequest,"selPlid"); // Recupero il Layout usando il plid Layout selLayout = LayoutLocalServiceUtil.getLayout(selPlid); UnicodeProperties layoutTypeSettings = null; if (selLayout != null) { // Se il Layout non è nullo, recupero le typeSettings layoutTypeSettings = selLayout.getTypeSettingsProperties(); } %> <liferay-ui:error-marker key="error-section" value="my-custom-fields-to-manage-error-message" /> <aui:model-context bean="<%= selLayout %>" model="<%= Layout.class %>" /> <h3><liferay-ui:message key="my-custom-fields-to-manage-title" /></h3><aui:fieldset cssClass="lfr-portrait-editor"> <% String riskType = GetterUtil.getString(layoutTypeSettings.getProperty("risk-type")); %> <aui:select label="risk-type" name="TypeSettingsProperties--risk-type--" showemptyoption="<%= true %>"> <aui:option label="type-car" selected="<%= "CAR".equals(riskType) %>" value="<%=Constants.CAR%>"/> <aui:option label="type-motorbike" selected="<%= "MOTORBIKE".equals(riskType)%>" value="<%=Constants.MOTORBIKE%>"/> <aui:option label="type-easy" selected="<%= "EASY".equals(riskType) %>" value="<%= Constants.EASY %>"/> <aui:option label="type-quote" selected="<%= "QUOTE".equals(riskType) %>" value="<%= Constants.VERTIQUOTE %>"/> <aui:option label="type-home" selected="<%= "HOME".equals(riskType) %>" value="<%= Constants.HOME %>"/> <aui:option label="type-other-vehicle" selected="<%= "OTHER_VEHICLE".equals(riskType) %>" value="<%=Constants.OTHER_VEHICLE%>"/> <aui:option label="type-ivass" selected="<%= "IVASS".equals(riskType) %>" value="<%= Constants.IVASS %>"/> </aui:select> </aui:fieldset>

    Ed eccolo qui, l'utimo tassello del puzzle! :)

    Una nota importante: come potete vedere, nel nome della select è stato inserito:

     TypeSettingsProperties--risk-type--

    che i più attenti di voi avranno già riconosciuto come un meccanismo "automatico" che viene usato dal portale per leggere e salvare arbitrari valori che arrivano dal web (di fatto è la stessa convenzione che si utilizza sul salvataggio delle configurazioni!).

    Detto questo, ovviamente, deploy, navigazione, test e... Funziona! ;)

    A questo punto, anche voi (come noi :D), adesso potete utilizzare le typeSettings di pagina come un modo più elegante e furbo per far configurare parametri al Cliente.

    .. Sempre che non abbiate una fretta del diavolo e i Custom Attribute non vi sembrino molto più smart e semplici da utilizzare!!

    Direi che anche per oggi è tutto: se avete dubbi o domande, come sempre, sono a vostra disposizione; scriveteci nei commenti e fateci sapere che ne pensate!

    Buona giornata a tutti! ;)

  4.  

    Ciao a tutti!

    Il problema di oggi è una roba che molti di voi faranno da una vita; siccome però mi sono trovato su un progetto a farlo, ci ho messo un po' ma sono arrivato anch'io! :)

    Il problema è molto semplice: abbiamo un componente sul frontend che triggera il caricamento via AJAX di una porzione di HTML che, tuttavia, contiene del JS e che quindi dev'essere parsato.

    Ovviamente, poi, se quando invio la form ci sono degli errori di validazione, quando torno in pagina dovrei:

    • trovare caricato il frammento di HTML;
    • trovare valorizzato il frammento di HTML con i valori selezionati.

    Nel mio caso funzionale, l'obiettivo era che, dopo aver utilizzato l'inline search, dall'id dell'oggetto selezionato dovevo caricare tutte le entità figlie ad esso collegate e:

    • permettere di selezionarli singolarmente;
    • permettere di selezionarli tutti in un click.

    Per non reinventare la ruota, l'idea che mi è venuta era quella di utilizzare il SearchContainer insieme alla funzionalità del RowChecker, così che mi venisse gratis tutta la parte di codice di impaginazione dei figli ma anche, appunto, la possibilità di selezioare tutti o alcuni dei record visualizzati.

    Però sono in una JSP già caricata.. E come faccio a caricare questa roba, che, siccome parliamo di SearchContainer, dev'essere renderizzata usando le taglib di portale?

    Beh, la risposta è semplice: AJAX!

    Ok, ma.. Come? :)

    Cercando un po' sui progetti che abbiamo fatto, mi sono ricordato di una roba che aveva fatto Paolo Gambetti e che avevo molto elegante; quindi ho recuperato tutto e messo tutto insieme!

    Vediamo ora un po' di codice..

    Caricamento asincrono via AJAX dell'HTML (con relativo parsing)

    La prima cosa che ho fatto, è stata quella di mappare in una funzione JS di pagina, la logica di caricamento e popolamento del componente. Questo l'ho fatto ovviamente perché devo gestire due casi:

    1. quando si carica la pagina la prima volta e, al trigger sul front end, devo caricare il frammento HTML;
    2. quando si ricarica la pagina e il componente deve riapparire popolato!

    La funzione JS è molto semplice:

      Liferay.provide(window, '<portlet:namespace/>loadSalesPoints', function(customerId) { // Questa chiamata serve perché una volta che ho caricato il componente, // questo viene registrato e al caricamento successivo ho un errore; // ma se lo rimuovo funziona tutto! :) Liferay.destroyComponent('<portlet:namespace/>salespointsSearchContainer'); customerIdField.val(customerId); var portletURL = Liferay.PortletURL.createRenderURL(); portletURL.setPortletId('<%=PortletKeys.CALENDAR %>'); portletURL.setPlid(<%= plid %>); portletURL.setWindowState('<%=LiferayWindowState.EXCLUSIVE.toString() %>'); portletURL.setParameter('customerId', customerId); portletURL.setParameter('mvcPath', '/html/calendar/excel/planCalendar/show_sales_points.jsp'); pvContainerField.plug(A.Plugin.IO, { failureMessage: 'In elaborazione...', parseContent: true, showLoading: true, after: { success: function(event) { <c:if test="<%= !SessionErrors.isEmpty(renderRequest) %>"> var salesPointId = "<%= ParamUtil.getString(renderRequest, "salesPointIds")%>"; var salesPointArray = salesPointId.split(','); // Recupero tutti i field con name "<portlet:namespace/>rowIds" // leggo i loro valori e se corrispondono setto il flag checked A.all('input[name=<portlet:namespace/>rowIds]').each(function (field) { for (var i = 0; i < salesPointArray.length; i++) { var arrValue = salesPointArray[i]; if (field.val() == arrValue) { field.setAttribute('checked', true); } } }); </c:if>  } }, uri: portletURL.toString(), where: 'replace' }); pvContainerField.io.start(); }, ['aui-base', 'aui-io-plugin-deprecated', 'liferay-portlet-url']);

    Come sicuramente avrete notato, ci sono questi accorgimenti:

    • nella definizione della funzione, uso il namespace per renderla univoca: questo viene fatto così se finsice in pagina più volte almeno viene sendboxata;
    • subito dopo il caricamento via AJAX del frammento HTML (after: success: {}) uso un <c:if/> per capire se sono tornato in pagina a causa di un errore oppure se sono in creazione; questo ovviamente mi serve per ripopolare il componente con i valori corretti;
    • la magia del caricamento avviene in automatico quando chiamo la funzione: è stata bindato sul componente A.Plugin.IO, che permette (anche se deprecato) il caricamento via AJAX dell'HTML che mi serve;
    • la magia del parsing mi viene offerta gratis sempre da A.Plugin.IO: grazie all'attributo parseContent: true viene attivato l'eval del JS nella pagina (figo!);
    • grazie alla direttiva where: replace, l'HTML che sarà servito lato server farà la sostituzione del mio markup.

    Direi che non c'è bisogno di molte altre spiegazioni; il codice è abbastanza semplice ma, se avete dubbi, lasciateli nei commenti che rispondiamo! ;)

    La JSP che viene caricata

    Beh, questa è proprio "semplice":

    Questo è il codice:

     <%@ page import="com.liferay.portal.kernel.dao.search.RowChecker" %> <%@ include file="/META-INF/resources/html/init.jsp"%> <% long customerId = ParamUtil.getLong(request, "customerId"); %> <liferay-ui:search-container delta="200" deltaconfigurable="false" emptyresultsmessage="no-entries-were-found"  rowChecker="<%=new RowChecker(renderResponse) %>" total="<%= SalespointLocalServiceUtil.countByG_C(scopeGroupId, customerId) %>"> <liferay-ui:search-container-results results="<%= SalespointLocalServiceUtil.findByG_C(scopeGroupId, customerId)%>"/> <liferay-ui:search-container-row classname="it.dvel.example.project.calendar.model.Salespoint" keyproperty="salespointId" modelvar="salesPoint"> <liferay-ui:search-container-column-text> <%= SalespointAddressFormatter.format(salesPoint)%> </liferay-ui:search-container-column-text> </liferay-ui:search-container-row> <liferay-ui:search-iterator paginate="false"/> </liferay-ui:search-container>

    Qui l'unica cosa degna di nota è l'abilitazione del RowChecker che ho evidenziato sul codice qui sopra!

    Ovviamente tutto questo funziona quando, da qualche parte nel mio JS in pagina, io chiamo la funzione che abbiamo mappato in precedenza:

    <portlet:namespace/>loadSalesPoints(result.id);

    E questo è tutto quello che dovrebbe servirvi per far funzionare il giro come indicato qui sopra! :)

    C'è ancora un punto, però, che secondo me vale la pena segnalare!

    Paginazione si, paginazione no: attenzione però al numero di risultati!

    Io nel mio caso sono stato fortunato; la numerica dei record figli è sempre molto bassa (al massimo 20 righe); perché vi dico questo, però?

    Beh, perché forse non lo sapete ma il SearchContainer ha una limitazione: non può caricare più di 200 record in una finestra (mi pare che fossero tipo 1.000 nella 6.2 ma nella 7 sono stati abbassati a 200).. Questo, ovviamente, per performance e buona gestione della memoria.

    Quindi ricordate: quando usate il SearchContainer e volete presentare in una botta sola tutti i record, fare in modo che il vostro numero massimo sia minore o uguale al limite che vi ho esposto sopra!

    Alla prossima! ;)

  5. Buongiorno a tutti e ben ritrovati!

    Questo è proprio un periodo felice: riesco a scrivere un sacco di non-post (come quello di ieri ma anche come quello che sto scrivendo ora.. :D) che affrontano tematiche che.. Interessano praticamente solo me! ;D

    In realtà quello di oggi è un topic che mi da un po' da fare da sempre; ho sempre avuto il pallino di abbandonare Eclipse (o il Liferay Developer Studio) in funzione di una IDE che fosse un po' più robusta, stabile, funzionante, chi-più-ne-ha-più-ne-metta... :D

    Scherzi a parte, senza nulla togliere al Liferay Developer Studio 2.2.2 (che ad oggi l'Ing. Napolitano lista ancora nella sua personale top list delle versioni del LDS -di cui è un attento e preciso catalogatore seriale.. :D-), avevo da un po' cominciato a guardare al tema ma senza successo.

    L'antefatto...

    Poi è successo un fatto.. Nel weekend ho fatto upgrade del sistema operativo senza fare backup (prima volta in 20 anni.. :/).

    Beh, Big Sur (ho un mac..) ha un piccolo glitch per cui non viene più impostata correttamente la JAVA_HOME.

    Poco male, direte voi, mica tu Jader sei uno che sviluppa.. :D

    ... Vero!

    Però, però, però, c'è un però.. :D

    ... Questa settimana stavo proprio tenendo un corso guardacaso sulla 6.2 di LR.

    E il fatto che non mi setti più la JAVA_HOME (anche se a manazza sembra averla correttamente settata..), il Liferay Developer Studio 2.2.2. NON mi parte più! 

    [N.d.J.: Appunto che ho correttamente configurato l'init di Eclipse impostando a manazza l'argomento per la corretta JVM, ma niente. Parte ma si frizza sullo splash screen e sulla selezione del workspace..]

    L'esigenza..

    Quindi quello che fino a sabato mattina era solo un vezzo, ora è diventato una triste necessità! ;)

    Per terminare il corso non ho avuto grossi problemi: ho riesumato il mac che avevo dismesso durante il lockdown in favore del nuovo e quindi il corso è terminato correttamente.

    Però ci sono alcuni progetti sui quali abbiamo della maintenance attiva per i quali, purtroppo o per fortuna :), dobbiamo fare manutenzione.

    [N.d.J.: Dobbiamo.. Devo: perché né l'Inge, né gli altri colleghi ne vogliono sapere.. :D]

    La soluzione

    Quindi questa mattina, di buon'ora (come sono solito fare da quando sono in smart working :)), mi sono messo d'impegno per far funzionare il LRSDK62 su IntelliJ.. 

    Spoiler: la soluzione che segue è ovvia, lo so, talmente ovvia che i più, tra voi, sicuramente la bolleranno come "minchiata" :). La scrivo comunque, però, perché un giorno mi servirà e allora non dovrò rifare tutto il giro che ho fatto questa mattina! :)

    Ragionandoci un po' su ho avuto un'intuizione: il DevStudio non fa altro che gestire dinamicamente il classpath e poi via ANT eseguire dei task.

    Se sistemo a manazza il classpath e faccio in modo che ANT buildi, vinco facile.. :)

    Quindi:

    • ho aperto in IntelliJ LE SINGOLE PORTLET contenute all'interno del SDK;
      • Quindi i progetti contenuti nelle varie folder del SDK (portlets, layouttpl, hooks, etc..);
    • ho configurato il classpath (more on this later...) delle singole portlet impostando:
      • il classpath globale con tutte le dipendenze per far compilare i miei portlet;
      • il classpath dei singoli moduli puntando alla loro WEB-INF/lib interna (quando necessario);
    • ho configurato ANT (la versione corretta) e l'ho fatto funzionare (solo da command line, sorry...)

    Dettagli tecnici

    Questo è lo screenshot di come ho configurato il classpath globale:

    Come sicuramente avrete notato bisogna aggiungere (per praticità / velocità):

    • tutti i JAR contenuti nella lib del Tomcat bundle Liferay;
    • tutti i JAR contenuti nella lib/ext del Tomcat bundle Liferay;
    • questi singoli JAR, presi dalla <tomcat home>/webapps/ROOT/WEB-INF/lib:
      • commons-logging.jar
      • jstl-api.jar
      • jstl-impl.jar
      • log4j.jar
      • util-bridges.jar
      • util-java.jar
      • util-taglib.jar

    Fatto questo gli artefatti vi compileranno già; rimane da sistemare ANT.

    Per farlo funzionare, mi sono scaricato la versione apache-ant-1.9.15 e, a mano, lancio i singoli task da dentro alle folder dei singoli plugin.Questo approccio funziona: compila e fa deploy correttamente!

    Conclusioni

    Sicuramente starete dicendo peste e corna di questo post ;), mi sembra quasi di sentirvi! :)

    Però dovete ammettere che di articoli che spiegano questa cosa non ce ne sono proprio su internet... Forse perché, come dicevo all'inizio, forse è un problema soltanto mio.. :D

    Aggiungo che tutto quanto sopra funziona perché io ho già un SDK che è correttamente configurato: intendo che al suo interno è già presente, parametrizzato correttamente dal Liferay Developer Studio, il file build.jed.properties (che è il file che permette ad ANT di funzionare correttamente).

    Se così non fosse, nel senso che state iniziando un progetto nuovo sulla 6.2, beh, il mio consiglio è FERMARVI e cominciare il vostro nuovo  progetto su LR7.x perché, e sono serio, la vostra vita sarà molto più semplice e divertente! ;)

    In ultima analisi, è ovvio che questo paradigma (LR62 + IntelliJ) è un palliativo: quello che si perde nel non usare il Liferay Developer Studio è tanta roba (snippet, pannellini di configurazione, wizard.. Tanta roba davvero!).

    È vero anche però, per contro, che il tempo che guadagnerete lavorando con questa configurazione sarà mostruosamente più alto del tempo che Eclipse -io parlo per me, magari la vostra esperienza è diversa..- vi fa perdere di solito.. :)

    Detto questo, sperando di avervi aiutato a migliorare un po' la vostra performance lavorativa (o aver risolto il problema che BUG Sur ha introdotto.. :)), vi lascio e vi do appuntamento alla prossima! ;)

    Divertitevi e buon IntelliJ a tutti (anche con LR62)!! ;)