Nel momento i cui cerchiamo di spingerci oltre quello che strumenti di sviluppo ci offrono di “default”, è quasi certo che incontreremo qualche difficoltà ad andare avanti e molto spesso la difficoltà è dovuta alla disponibilità di scarsa documentazione.

Nell’ambito del Service Layers, la piattaforma di sviluppo di Liferay consente in modo semplice e veloce di generare tutto lo strato dei servizi, sia locali sia remoti (Liferay Inc., 2014) per i plugin (Liferay Inc., 2014) da noi creati.

Nel corso di quest’articolo vedremo come sia possibile sfruttare il Service Builder (Liferay Inc., 2014) per creare dei servizi da esporre all’esterno non direttamente collegati alle entità gestiste dallo specifico plugin. L’articolo è rivolto a tutti, ma sicuramente a trarne maggior beneficio saranno coloro con un minimo di confidenza con l’SDK[1] (Liferay Inc., 2014)  di Liferay e in particolar modo con il Service Builder. In allegato all’articolo sarà disponibile l’intero progetto di modo che possiate analizzarlo con tutta calma e fare i vostri test.

Il progetto sviluppato per quest’articolo è stato realizzato e testato su di un’installazione Liferay 6.2 Community Edition e Liferay 6.2 Enterprise Edition.

Introduzione

In genere le portlet sviluppate sono data-driven e il Service Builder si rileva il nostro migliore “amico” perché fa al posto nostro tutto il lavoro di:

  • Generare model, persistenza e Service Layers;
  • Generare le interfacce locali e remote;
  • Generare la configurazione di Hibernate[2] e Spring[3];
  • Generare i finder method per le entità.

Per un attimo ipotizziamo l’esigenza di voler esporre (dalla nostra portlet) un servizio verso l’esterno accessibile tramite interfacce di tipo JSON[4] e SOAP[5], che sia un raggruppamento di servizi esistenti. Lo schema mostrato in Figura 1 dovrebbe aiutare a capir meglio l’idea espressa.

Figura 1 Portlet che espone un servizio custom.
Figura 1 Portlet che espone un servizio custom.

Quello che vogliamo ottenere è quindi un servizio personalizzato chiamato Custom Users Service disponibile pubblicamente e che sfrutti i servizi core del portale. In questo particolare scenario vogliamo fare in modo che il servizio Custom Users Service, esponga un metodo che ritorni al consumer del servizio, la lista di utenti taggati con un determinato tag (Liferay Inc., 2014).

Perché realizzare un servizio del genere? Alcune delle finalità principali possono essere:

  • Realizzare un servizio che implementi un requisito che non è possibile soddisfare attraverso un servizio core del portale;
  • Comporre più servizi core del portale per realizzare un servizio unico che sia di più facile utilizzo per il consumer;

Nello schema di Figura 2 è rappresentato il servizio Custom Users Service messo a disposizione dall’ipotetica portlet che ho chiamato Shirus Labs Example Services.

Figura 2 Dettaglio del servizio Custom Users Service.
Figura 2 Dettaglio del servizio Custom Users Service.

Qual è il trucco?

Il più delle volte (oserei dire sempre) il Service Builder è utilizzato per generare il codice e le configurazioni (Spring e Hibernate) necessarie per le operazioni di tipo CRUD[6] sull’entità gestite dalla portlet, ma nel nostro caso la parte Hibernate è da escludere, proprio perché non abbiamo a che fare con entità nostre. In Figura 3, nella sezione A è mostrato il tool del Service Builder in condizioni “normali”, al contrario, nella sezione B, manca la parte Hibernate. Il trucco è proprio nella sezione B, in altre parole, nella definizione dell’entità descritta sul file service.xml, occorre specificare esclusivamente il nome dell’entità e la disponibilità del tipo di servizio: locale, remoto o entrambe.  Nel Listato 1 è riportata la configurazione del Service Builder per la portlet del nostro esempio.

Figura 3 Il Service Builder Tool sopra Spring & Hibernate che genera codice e configurazione.
Figura 3 Il Service Builder Tool sopra Spring & Hibernate che genera codice e configurazione.

In Liferay, un’entità rappresenta solitamente una business facade[7] e una tabella sulla base di dati. Se l’entità non ha nessuna colonna definita, allora l’entità rappresenta semplicemente una business facade. In queste condizioni (così come mostrato nella configurazione del Listato 1) il Service Builder genererà una business facade POJO[8] vuota. Le successive esecuzioni del Service Builder verificheranno l’esistenza della business facade, qualora esistesse e con eventuali nuovi metodi, il Service Builder si preoccuperà di aggiornare anche i wrappers SOAP.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 6.2.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_6_2_0.dtd">
<service-builder package-path="it.dontesta.shirus.labs.liferay.ws.example">
	<author>amusarra</author>
	<namespace>shirus_labs_liferay_ws_example</namespace>
	<entity name="CustomUsers" local-service="true" remote-service="true">
	</entity>
	<exceptions>
		<exception>UsersNotFound</exception>
	</exceptions>
</service-builder>

Listato 1 Configurazione del Service Builder sul file service.xml.

Per l’entità CustomUsers definita nel file di configurazione service.xml indichiamo al Service Builder di generare le interfacce remote e le interfacce locali, inoltre chiediamo di generare un tipo di eccezione UsersNotFoundException che utilizzeremo in seguito per sollevare un eccezione nel caso in cui la ricerca non produca nessun risultato.

Qualora la vostra curiosità vada oltre, consiglio di consultare il DTD[9] che si riferisce al Service Builder. Dalla lettura del DTD potete scoprire un bel po’ d’informazioni che potrebbero tornare utili.

Eseguito il Service Builder, siamo pronti per l’implementazione del servizio Custom Users Service.

Implementazione del servizio Custom Users Service

Qualcuno di voi saprà certamente che, implementare il servizio Custom Users Services significa scrivere l’implementazione dell’interfaccia, locale (Liferay Inc., 2014) e remota (LIferay Inc., 2014), ed esattamente:

  • CustomUsersLocalService: Interfaccia locale di cui implementare il metodo getUsersCompanyByTagName;
  • CustomUsersService: Interfaccia remota di cui implementare il metodo getUsersCompanyByTagName.

 

Servizio Metodo & Descrizione

Parametri del servizio

CustomUsersService getUsersCompanyByTagName.
Quest’operazione del servizio restituisce sulla base dei parametri in input una lista di utenti del portale che soddisfano le condizioni di ricerca.
Param Type Descrizione
companyId Long CompanyId d’interesse per la ricerca degli utenti.
tagName String Nome del tag assegnato agli utenti che si vuole ricercare (esempio: CRM, Imported, etc…).
userStatus Int Stato dell’utente (esempio: Attivo, disattivo etc…).
start Int Start index
end Int End index

Tabella 1 Descrizione del servizio Custom Users Service da implementare.

Dopo l’esecuzione del Service Builder, avremo a nostra disposizione una serie di classi e interfacce generate a supporto del nostro servizio. Il cuore del servizio è la classe CustomUsersLocalServiceImpl, all’intero della quale dovrà essere codificata la logica di business del nostro servizio. In Figura 4 è mostrato il class diagram delle classi e interfacce generate dal Service Builder, utile per capire le relazioni tra i vari elementi. Per questioni di spazio e chiarezza, il diagramma riporta solo la parte local del servizio. Com’è possibile notare dal diagramma di Figura 4, la classe CustomUsersLocalServiceImpl e l’interfaccia CustomUsersLocalService presentano già il metodo da implementare, questo perché il diagramma è stato generato dopo l’implementazione del metodo e la successiva esecuzione del Service Builder, inoltre è stato anche aggiunto il metodo statico alla classe CustomUsersLocalServiceUtil.

Figura 4 Class diagram interfacce e classi (parte locale) generate dal Service Builder.
Figura 4 Class diagram interfacce e classi (parte locale) generate dal Service Builder.

Ricordiamo che dal metodo da implementare desideriamo ottenere l’elenco degli utenti che hanno un determinato tag, apposto per esempio durante l’importazione (Musarra, 2014) di un insieme di utenti provenienti da qualche altro sistema (CRM, ERP, etc…).

L’implementazione del metodo getUsersCompanyByTagName è abbastanza semplice, si traduce in pochissime righe di codice, grazie anche all’utilizzo del servizio di ricerca del SearchEngine (Liferay Inc., 2014) di Liferay, così facendo la risposta del servizio sarà anche più performante. La lista degli utenti che il metodo restituirà (sotto forma di array) sarà del tipo com.liferay.portal.model.UserSoap, questo perché vogliamo che il servizio sia disponibile tramite SOAP, quindi dobbiamo utilizzare il wrapper SOAP dell’entità User che Liferay ci mette a disposizione, e inoltre eviterà problemi di serializzazione e deserializzazione.

Il Listato 2 riporta l’implementazione del metodo getUsersCompanyByTagName per della classe CustomUsersLocalServiceImpl, mentre il Listato 3 riporta l’implementazione del metodo della classe CustomUsersServiceImpl.

public com.liferay.portal.model.UserSoap[] getUsersCompanyByTagName(long companyId, String tagName,
			Integer userStatus, int start, int end) throws PortalException,
			SystemException {
 
		
	SearchContext searchContext = new SearchContext();
 
	searchContext.setCompanyId(companyId);
	searchContext.setEntryClassNames(new String[] { User.class.getName() });
 
	searchContext.setStart(start);
	searchContext.setEnd(end);
	searchContext.setAndSearch(true);
		
	BooleanQuery searchQuery = BooleanQueryFactoryUtil.create(searchContext);
	searchQuery.addRequiredTerm(Field.COMPANY_ID, companyId);
	searchQuery.addRequiredTerm(Field.ASSET_TAG_NAMES, tagName);
	searchQuery.addRequiredTerm(Field.STATUS, userStatus);
		
	Hits hits =  SearchEngineUtil.search(searchContext, searchQuery); 
	Tuple	tuple = UsersAdminUtil.getUsers(hits);
 
	if (hits.getLength() == 0) {
		throw new UsersNotFoundException("No data available for the search parameters you set.");
	}
 
	List<User> results = (List<User>) tuple.getObject(0);
	UserSoap[] userSoap = new UserSoap[results.size()];
 
		
	for (int i = 0; i < userSoap.length; i++) {
		userSoap[i] = UserSoap.toSoapModel(results.get(i));
	}
				
	return userSoap;
}

Listato 2 Implementazione del metodo getUsersCompanyByTagName per l’interfaccia locale.

public UserSoap[] getUsersCompanyByTagName(long companyId, String tagName,
			Integer userStatus, int start, int end) throws PortalException,
			SystemException {
		
	return CustomUsersLocalServiceUtil.getUsersCompanyByTagName(companyId, tagName, userStatus, start, end);
}

Listato 3 Implementazione del metodo getUsersCompanyByTagName per l’interfaccia remota.

Test del servizio Custom Users Service

Adesso che il servizio è bello che confezionato e installato sul portale di Liferay, è possibile accedervi da remoto tramite l’interfaccia SOAP e JSON. Gli end point del servizio esposto dalla portlet sono:

  • SOAP http://[host]:[port]/ api/axis/Plugin_shirus_labs_liferay_ws_example_CustomUsersService
  • JSON http://[host]:[port]/api/jsonws/ShirusLabsExampleServices-portlet.customusers/get-users-company-by-tag-name

Se volessimo eseguire un test veloce del servizio SOAP, è più che sufficiente prendere nota dell’end point e utilizzare il tool soapUI. In Figura 5 è mostrato un test del servizio eseguito proprio con soapUI.

Figura 5 Test del servizio tramite il tool soapUI.
Figura 5 Test del servizio tramite il tool soapUI.

Tramite ant è possibile senza alcuno sforzo generare un client Java da poter utilizzare all’interno della vostra applicazione da dover integrare. Il task ant che assolve questo compito si chiama build-client. L’esecuzione del task genererà il jar (che nel nostro caso prende il nome di ShirusLabsExampleServices-portlet-client.jar) che potrete utilizzare per consumare il nuovo servizio dalla vostra applicazione. L’esecuzione del task ant posizionerà il jar del client dentro la directory docroot/WEB-INF/client del progetto. Nel Listato 4 le poche righe di codice mostrano come sia semplice e immediato accedere al servizio.

public class TestGetUsersCompanyByTagNameService {
 
	static final String LIFERAY_USER_NAME = (System.getProperty("username") != null) ? System
			.getProperty("username") : "test@liferay.com";
	static final String LIFERAY_USER_PASSWORD = (System.getProperty("password") != null) ? System
			.getProperty("username") : "test";
 
	static final String USER_SERVICE = "Portal_UserService";
 
	/**
	 * 
	 */
	public TestGetUsersCompanyByTagNameService() {
	}
	
	public static void main(String[] args) throws Exception, SystemException {
		CustomUsersServiceSoapServiceLocator locator = new CustomUsersServiceSoapServiceLocator();
		CustomUsersServiceSoap customUsersService = locator
				.getPlugin_shirus_labs_liferay_ws_example_CustomUsersService();
 
		((Plugin_shirus_labs_liferay_ws_example_CustomUsersServiceSoapBindingStub) customUsersService)
				.setUsername(LIFERAY_USER_NAME);
		((Plugin_shirus_labs_liferay_ws_example_CustomUsersServiceSoapBindingStub) customUsersService)
				.setPassword(LIFERAY_USER_PASSWORD);
 
		UserSoap[] listUsers = customUsersService.getUsersCompanyByTagName(
				10157l, "crm", 0, 0, 10);
 
		for (UserSoap userSoap : listUsers) {
			System.out.println("User ScreeName : " + userSoap.getScreenName());
		}
	}
}

Listato 4 Esempio di accesso al servizio SOAP tramite il client generato dall’SDK.

Per quanto riguarda il servizio JSON, è possibile eseguire un semplice test tramite il tool cURL[10] o direttamente via browser attraverso l’opportuna URL in stile RESTful[11]. Il Listato 5 mostra l’esempio di accesso al servizio JSON tramite cURL.

curl http://localhost:8080/api/jsonws/ShirusLabsExampleServices-portlet.customusers/get-users-company-by-tag-name \
-u test@liferay.com:test \
-d companyId=10157 \
-d tagName='crm' \
-d userStatus=0 \
-d start=0 \
-d end=3

Listato 5 Test del servizio JSON tramite curl.

Per ottenere lo stesso risultato del Listato 5 via URL, è sufficiente specificare sul vostro browser la seguente URL: http://localhost:8080/api/jsonws/ShirusLabsExampleServices-portlet.customusers/get-users-company-by-tag-name/company-id/10157/tag-name/crm/user-status/0/start/0/end/3 per ottenere un risultato simile a quello mostrato nel Listato 6.

[{
        "agreedToTermsOfUse": false, 
        "comments": "", 
        "companyId": 10157, 
        "contactId": 12102, 
        "createDate": 1392734987656, 
        "defaultUser": false, 
        "digest": "", 
        "emailAddress": "antonio.musarra@gmail.com", 
        "emailAddressVerified": false, 
        "facebookId": 0, 
        "failedLoginAttempts": 1, 
        "firstName": "Antonio", 
        "graceLoginCount": 0, 
        "greeting": "Welcome Antonio Musarra!", 
        "jobTitle": "IT Architect", 
        "languageId": "en_US", 
        "lastFailedLoginDate": 1393625990890, 
        "lastLoginDate": null, 
        "lastLoginIP": "", 
        "lastName": "Musarra", 
        "ldapServerId": -1, 
        "lockout": false, 
        "lockoutDate": null, 
        "loginDate": null, 
        "loginIP": "", 
        "middleName": "", 
        "modifiedDate": 1392735025523, 
        "openId": "", 
        "password": "AAAAoAAB9ABtc0QtttzLGtK931wwN957psU8maq0Jxu1Gais", 
        "passwordEncrypted": true, 
        "passwordModifiedDate": 1392734988948, 
        "passwordReset": true, 
        "portraitId": 0, 
        "primaryKey": 12101, 
        "reminderQueryAnswer": "", 
        "reminderQueryQuestion": "", 
        "screenName": "amusarra", 
        "status": 0, 
        "timeZoneId": "UTC", 
        "userId": 12101, 
        "uuid": "baf84339-315b-426c-838b-e58dc806bec8"
    }]

Listato 6 Esempio di risposta del servizio JSON.

Conclusioni

Abbiamo fatto un breve excursus su come sia possibile e semplice realizzare dei servizi custom da esporre verso l’esterno che utilizzano servizi del portale già preesistenti. In questo modo è possibile realizzare in poco tempo dei servizi anche complessi come composizione di altri servizi e rendere in questo modo l’interfaccia verso il sistema client molto più semplice da utilizzare e snella.


[1] Un software development kit (in acronimo SDK, traducibile in italiano come “pacchetto di sviluppo per applicazioni“), in informatica, indica genericamente un insieme di strumenti per lo sviluppo e la documentazione di software.

[2] E’ una piattaforma middleware open source per lo sviluppo di applicazioni Java, attraverso l’appoggio al relativo framework, che fornisce un servizio di Object-relational mapping (ORM) ovvero gestisce la persistenza dei dati sul database attraverso la rappresentazione e il mantenimento su database relazionale di un sistema di oggetti Java.

[3] In informatica Spring è un framework open source per lo sviluppo di applicazioni su piattaforma Java.

[4] JSON, acronimo di JavaScript Object Notation, è un formato adatto per lo scambio dei dati in applicazioni client-server.

[5] In informatica SOAP (inizialmente acronimo di Simple Object Access Protocol) è un protocollo leggero per lo scambio di messaggi tra componenti software, tipicamente nella forma di componentistica software. La parola object manifesta che l’uso del protocollo dovrebbe effettuarsi secondo il paradigma della programmazione orientata agli oggetti.

[6] La tavola CRUD (Create, Read, Update, Delete) associa utenti e risorse, o loro aggregazioni, di un sistema informatico indicando i relativi privilegi di accesso.

[7] Letteralmente façade significa “facciata”, ed infatti nella programmazione ad oggetti indica un oggetto che permette, attraverso un’interfaccia più semplice, l’accesso a sottosistemi che espongono interfacce complesse e molto diverse tra loro, nonché a blocchi di codice complessi.

[8] Plain Old Java Object (POJO) ovvero un semplice JavaBean costituito dalle proprietà e i rispettivi getters/setter (quando serve).

[9] Il Document Type Definition (definizione del tipo di documento) è uno strumento utilizzato dai programmatori il cui scopo è quello di definire le componenti ammesse nella costruzione di un documento XML.

[10] cURL è un tool Open Source a linea di comando che consente il trasferimento di dati utilizzando svariati protocolli.

[11] REpresentational State Transfer (REST) è un tipo di architettura software per i sistemi di ipertesto distribuiti come il World Wide Web

Bibliografia

Liferay Inc. (2014). Generating Your Service Layer. Tratto da Liferay Portal 6.2 Developer’s Guide: http://www.liferay.com/it/documentation/liferay-portal/6.2/development/-/ai/generating-your-service-layer-liferay-portal-6-2-dev-guide-04-en

Liferay Inc. (2014). Plugin Management. Tratto da Using Liferay Portal 6.2: http://www.liferay.com/it/documentation/liferay-portal/6.2/user-guide/-/ai/plugin-management-liferay-portal-6-2-user-guide-14-en

Liferay Inc. (2014). Searching for Content in Liferay. Tratto da Using Liferay Portal 6.2: https://www.liferay.com/it/documentation/liferay-portal/6.2/user-guide/-/ai/searching-for-content-in-liferay-liferay-portal-6-2-user-guide-06-en

Liferay Inc. (2014). Tagging and Categorizing Content. Tratto da Using Liferay Portal 6.2: http://www.liferay.com/it/documentation/liferay-portal/6.2/user-guide/-/ai/tagging-and-categorizing-content-liferay-portal-6-2-user-guide-06-en

Liferay Inc. (2014). What is Service Builder? Tratto da Liferay Portal 6.2 Developer’s Guide: http://www.liferay.com/it/documentation/liferay-portal/6.2/development/-/ai/what-is-service-builder-liferay-portal-6-2-dev-guide-04-en

Liferay Inc. (2014). Working with Liferay’s Developer Tools. Tratto da Liferay Portal 6.2 Developer’s Guide: http://www.liferay.com/it/documentation/liferay-portal/6.2/development/-/ai/working-with-liferays-developer-tools-liferay-portal-6-2-dev-guide-02-en

Liferay Inc. (2014). Writing Local Service Classes. Tratto da Liferay Portal 6.2 Developer’s Guide: http://www.liferay.com/it/documentation/liferay-portal/6.2/development/-/ai/write-local-service-classes-liferay-portal-6-2-dev-guide-04-en

LIferay Inc. (2014). Writing Remote Service Classes. Tratto da Liferay Portal 6.2 Developer’s Guide: http://www.liferay.com/it/documentation/liferay-portal/6.2/development/-/ai/write-remote-service-classes-liferay-portal-6-2-dev-guide-04-en

Musarra, A. (2014, Jan 01). Liferay Web Services – Come importare utenti da un foglio Excel. Tratto da LinkedIn Corporation: http://www.slideshare.net/amusarra/liferay-web-services-come-importare-utenti-da-un-foglio-excel

1 Posts

Ho iniziato il mio viaggio nel mondo dell'informatica da un PC Olivetti M24 (http://it.wikipedia.org/wiki/Olivetti_M24) acquistato da mio padre per il suo lavoro. Giorno dopo giorno, ho rapidamente preso il controllo di quella "scatola" facendola mia. Nei giorni nostri mi occupo di consulenze per lo sviluppo di progetti in ambito enterprise utilizzando per lo più tecnologie web-oriented come JavaEE, Web Services, ESB, TIBCO, PHP. In questi ultimi anni sono focalizzato su ambienti Enterprise Portal e CRM, in particolare su temi d'integrazione tra i due ambienti adottando piattaforme ESB tipo WSO2 e Mule.