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.
Eccoci qua, di nuovo! Siamo stati un pò assenti ultimamente ma, buon per noi, abbiamo avuto parecchio da fare.
Oggi parliamo di OSGi e di un problema spinoso che tutti abbiamo incontrato almeno una volta: la dipendenza circolare ossia quella sfortunata casistica per cui un bundle A dipende da B e il bundle B dipende da A, cosa che ne impedisce il corretto funzionamento. A volte la dipendenza è diretta tra 2 bundle, altre volte invece riguarda più bundle ma sempre problematica è.
Spesso la soluzione alla dipendenza circolare è quella di realizzare un terzo bundle C che abbia entrambe le dipendenze A e B; purtroppo però non potevo seguire questa strada perché avrei dovuto cimentarmi in un refactor massiccio del codice e non ne avevo la possibilità (e ovviamente la voglia ).
Così ho cercato altre strade e ho scelto quella degli eventi OSGi. Ma cosa sono gli eventi OSGi? Sono un sistema, fornito dal container, per creare un canale di comunicazione tra i bundle mantenendoli però disaccoppiati in termini di dipendenze (come il Message Bus di Liferay per intenderci perché sì, stavo lavorando con Liferay 7.3).
Premetto però una cosa: gli eventi OSGi non sono stati creati specificatamente per risolvere il problema della dipendenza circolare, però in caso di necessità possono gestire il problema in modo abbastanza elegante.
L'idea alla base è molto semplice tutto sommato: il bundle A prepara l'evento con tutte le property che possono servire e invia l'evento sul bus OSGi. Il bundle B implementa un event handler che riceve l'evento, lo spacchetta e fa quello che deve fare, un pò come avviene in un qualunque sistema di gestione delle code.
L'invio dell'evento può avvenire sia in modalità asincrona che in modalità sincrona a seconda delle necessità : in entrambi i casi però non è previsto il ritorno di un risultato.
Vediamo quindi come fare per creare e inviare un evento OSGi dal bundle A.
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; import java.util.Dictionary; import java.util.Hashtable; @Component(immediate = true, service = MiaClasse.class) public class MiaClasse { @Reference private EventAdmin _eventAdmin; public void mioMetodo(long value1, String value2, boolean value3) { // Parametri da inviare all'event handler Dictionary<String, Object> props = new Hashtable<>(); props.put("param1", value1); props.put("param2", value2); props.put("param3", value3); // Creazione dell'evento OSGi Event event = new Event("nome/del/topic/dell/evento", props); // Invio asincrono dell'evento; il codice prosegue immediatamente _eventAdmin.postEvent(event); // Oppure in alternativa... // Invio sincrono dell'evento; il codice resta in attesa finché l'event handler non ha terminato _eventAdmin.sendEvent(event); } }
Come possiamo vedere il codice è piuttosto semplice: è sufficiente istanziare una mappa per i parametri, un oggetto Event e inviarlo. L'unica vera accortezza da tenere è il nome dell'evento (o topic in gergo OSGi) che deve seguire la sintassi indicata ossia niente spazi o punti ma stringhe separate da slash; è un pò come la destination di Liferay.
Ora vediamo invece il codice dell'event handler da implementare nel bundle B.
import com.liferay.portal.kernel.util.GetterUtil; import org.osgi.service.component.annotations.Component; import org.osgi.service.event.Event; import org.osgi.service.event.EventHandler; @Component(property = { "event.topics=nome/del/topic/dell/evento" }, service = EventHandler.class) public class MioEventHandler implements EventHandler { @Override public void handleEvent(Event event) { // Recupero dei parametri dell'evento long value1 = GetterUtil.getLong(event.getProperty("param1")); String value2 = GetterUtil.getString(event.getProperty("param2")); boolean value3 = GetterUtil.getBoolean(event.getProperty("param3")); // Esecuzione del codice applicativo // Nessun valore di ritorno! } }
Anche in questo caso le cose sono piuttosto semplici:
Tutto questo avviene in maniera totalmente disaccoppiata, senza che i bundle A e B si "conoscano" o abbiano dipendenze reciproche.
Bello ma se avessi davvero bisogno di un valore di ritorno? Il meccanismo degli eventi OSGi non lo prevede ma lavorando un pò con classi anonime ed espressioni lambda riusciamo di fatto a implementare un meccanismo di callback come se fosse Javascript; vediamo quindi come modificare il codice!
Innanzitutto supponiamo che il valore di ritorno che ci serve sia un booleano (chiaramente adeguate il codice alle vostre esigenze) e definiamo un'interfaccia che ce lo faccia gestire.
public interface BooleanResultHandler { // Il parametro è il valore di ritorno void onResult(boolean result); }
Ora invece vediamo come modificare la classe che genera l'evento.
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; import java.util.Dictionary; import java.util.Hashtable; import java.util.concurrent.atomic.AtomicBoolean @Component(immediate = true, service = MiaClasse.class) public class MiaClasse { @Reference private EventAdmin _eventAdmin; public boolean mioMetodo(long value1, String value2, boolean value3) { // Per gestire il valore di ritorno con coerenza durante tutto il flusso // non basta una variabile final ma dobbiamo scomodare gli oggetti Atomic AtomicBoolean returnValue = new AtomicBoolean(false); // Sfruttando l'espressione lambda, definisco la classe anonima di callback // che sarà usata dall'event handler per passare il valore di ritorno BooleanResultHandler callback = result -> returnValue.set(result); // Parametri da inviare all'event handler a cui aggiungo la callback Dictionary<String, Object> props = new Hashtable<>(); props.put("param1", value1); props.put("param2", value2); props.put("param3", value3); props.put("callback", callback); // Nuovo parametro dell'evento! // Creazione dell'evento OSGi Event event = new Event("nome/del/topic/dell/evento", props); // Invio sincrono dell'evento; il codice resta in attesa finché l'event handler non ha terminato // DEVE essere sincrono se vogliamo gestire il valore di ritorno! _eventAdmin.sendEvent(event); return resultValue.get(); } }
Le modifiche al codice non sono poi così tante: si tratta di definire una classe di callback da inviare all'event handler in modo da salvare il valore di ritorno.
Infine vediamo cosa deve fare l'event handler per fornirci il valore di ritorno.
import com.liferay.portal.kernel.util.GetterUtil; import org.osgi.service.component.annotations.Component; import org.osgi.service.event.Event; import org.osgi.service.event.EventHandler; @Component(property = { "event.topics=nome/del/topic/dell/evento" }, service = EventHandler.class) public class MioEventHandler implements EventHandler { @Override public void handleEvent(Event event) { // Recupero dei parametri dell'evento long value1 = GetterUtil.getLong(event.getProperty("param1")); String value2 = GetterUtil.getString(event.getProperty("param2")); boolean value3 = GetterUtil.getBoolean(event.getProperty("param3")); BooleanResultHandler callback = (BooleanResultHandler) event.getProperty("callback"); // Esecuzione del codice applicativo // che terminerà con la valorizzazione del valore di ritorno boolean result =... // Ecco finalmente il valore di ritorno inviato alla classe di callback e NON restituito dal metodo callback.onResult(result); } }
Ecco quindi concluso tutto il giro!
La prima classe genera l'evento insieme alla callback e poi aspetta (perché l'invio è sincrono).
La seconda classe riceve l'evento, fa quello che deve fare e poi invoca il metodo della callback; questo fa sì che sulla prima classe venga salvato il valore di ritorno che può quindi essere usato.
E per oggi è tutto! Spero di avervi risolto un problema.
Enjoy!
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.
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!
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.
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!
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.
Ed ecco la mia possibile soluzione, con la speranza che possa tornare utile anche a voi!
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! :)
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 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).
Per riuscire a fare questa implementazione, i vecchi developer avevano proceduto in questo modo:
form-navigator
della gestione delle pagine;form-navigator
avevano messo la loro bella JSP;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:
Cavolo, allora la sfida si faceva interessante! :)
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! ;)
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:
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:
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..
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:
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:
namespace
per renderla univoca: questo viene fatto così se finsice in pagina più volte almeno viene sendboxata;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;A.Plugin.IO
, che permette (anche se deprecato) il caricamento via AJAX dell'HTML che mi serve;parseContent: true
viene attivato l'eval del JS nella pagina (figo!);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! ;)
Beh, questa è proprio "semplice":
SearchContainer
e abilito il RowChecker
.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!
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! ;)