Miriade utilizza con successo Liferay da diversi anni, in vari scenari e per diversi clienti, sia come soluzione di portale istituzionale sia come strumento di lavoro interno.
Liferay è una piattaforma software open source basata su Java, progettata per la creazione di portali web aziendali e applicazioni digitali personalizzabili. È utilizzata principalmente per sviluppare intranet, extranet, siti pubblici e soluzioni di digital experience.
Originariamente noto come un portal framework, permette di integrare contenuti, servizi e applicazioni in un unico ambiente centralizzato. La sua architettura modulare consente di aggiungere funzionalità tramite plugin o moduli OSGi.
Le funzionalità principali includono gestione dei contenuti (CMS), gestione utenti e permessi avanzati, workflow, motore di ricerca interno, supporto per multilingua e responsive design. Liferay supporta anche l’integrazione con sistemi esterni (CRM, ERP, LDAP) e fornisce API REST e GraphQL per lo sviluppo headless.
Viene distribuito in due versioni:
- Liferay CE (Community Edition): gratuita e open source
- Liferay DXP (Digital Experience Platform): a pagamento, con supporto commerciale e funzionalità enterprise avanzate.
Il Contesto
Uno dei punti di forza di Liferay è la disponibilità di una SDK che permette di:
- sviluppare moduli custom, sia di backend sia di frontend;
- sviluppare personalizzazioni di moduli o servizi core;
- sviluppare estensioni per alcuni dei suoi framework, come ad esempio:
- modalità e protocolli di autenticazione
- modalità e protocolli di pagamento (per la parte commerce)
- tipologie di repository documentali
- altro
- Sviluppare temi e layout grafici custom.
Una delle principali tipologie di modulo custom utilizzato è chiamato “Service Builder”, uno strumento di generazione automatica del codice che semplifica la creazione del livello di accesso ai dati e dei servizi business logic all’interno della piattaforma.
Di fatto è un framework, integrato in Liferay, (basato su Java e OSGi) che permette di definire modelli di entità (Entity) in un file XML (service.xml
) e di generare automaticamente il codice Java necessario per:
- la persistenza dei dati (DAO e utilità per l’accesso al database);
- la creazione di servizi locali e remoti;
- la gestione dei metodi CRUD;
- il supporto per transazioni, sicurezza, e scalabilità.
Le caratteristiche principali del Service Builder sono:
Generazione automatica del codice
Da una semplice definizione XML, vengono generati i layer DAO, service e utility con codice strutturato e manutenibile.
Supporto per servizi locali e remoti
- LocalService (invocabile solo all'interno della stessa JVM)
- RemoteService (esposto tramite API SOAP/JSON)
Integrazione con il database
Crea automaticamente le tabelle nel database alla prima esecuzione del deployment, e permette anche di gestire il versionamento della struttura del database.
Gestione avanzata delle transazioni
Consente di definire il comportamento transazionale per ciascun metodo.
Modularità OSGi
I servizi generati sono registrati dinamicamente come componenti OSGi, rendendo il codice più modulare e componibile.
Estendibilità
I servizi generati possono essere personalizzati e arricchiti tramite override o aggiunta di metodi custom.
Integrazione con Liferay Permissions
Supporta l'integrazione con il sistema di permessi di Liferay per applicare controlli su risorse e azioni.
In sintesi, il Service Builder è una componente chiave per sviluppare applicazioni robuste su Liferay, riducendo il boilerplate e migliorando la coerenza dell’architettura.
Il Problema
In questo contesto, in alcuni scenari potrebbe essere utile per motivi di business implementare ricerche avanzate su queste entity custom, per le quali eventuali query su database potrebbero risultare complesse o poco performanti. In questo caso Liferay permette di sfruttare l’integrazione con il suo sistema di indicizzazione e ricerca.
Ricordiamo che Liferay 7.4 utilizza come motore di ricerca principale Elasticsearch, e che recentemente è uscito anche il plugin per il supporto ad Opensearch.
Questi due motori sono sufficientemente simili in termini di utilizzo da permettere a Liferay di utilizzare un unico framework che astrae l’implementazione e permette allo sviluppatore di utilizzare il motore di ricerca.
Per le principali entity “core” di Liferay, il portale già implementa tutte queste funzioni. Ma come posso fare, come sviluppatore, ad integrare le mie entity ed i miei sviluppi con questo framework?
Gli Obiettivi
L’integrazione riguarda tre aspetti principali.
- Aggiunta all’indice delle entity custom sviluppate tramite il service builder.
- Integrare le custom entity nello strumento di ricerca “generica” di Liferay.
- Eseguire ricerche custom ed elaborare i risultati.
NOTA
Per questo articolo useremo come esempio un servizio fatto in un nostro recente progetto, il cui service builder si occupa di salvare a database alcuni dati di prodotto che vogliamo indicizzare, che corrispondono ad una entity chiamata PageEntry.
Parte della logica è ovviamente dettata dalle necessità di contesto, e può essere più semplice in altri casi.
La Soluzione
Visto che gli obiettivi sono 3, andiamo per ordine.
1. Aggiunta all’indice di entity custom
La prima cosa da fare in questo caso è andare a fare un override dei principali metodi CRUD all’interno del -LocalService: cioè:
- addPageEntry(PageEntry entity)
- updatePageEntry(PageEntry entity)
- deletePageEntry(PageEntry entity)
- deletePageEntry(long pageEntryId)
L’override è necessario per poter aggiungere alla firma del metodo l’annotation Indexable, nelle sue due versioni:
JAVA
@Indexable(type = IndexableType.REINDEX)
// per gli add e gli update
@Indexable(type = IndexableType.DELETE)
// per i delete
Attenzione che queste annotazioni dovranno essere messe anche su eventuali metodi custom che facciano direttamente scritture senza passare attraverso uno dei metodi standard.
Una volta fatto questo è necessario specificare al framework quali campi e con quali caratteristiche andare a scrivere nell’indice, e questo può essere fatto in due modalità:
- Quella più tradizionale, andando ad implementare una classe che estende BaseIndexer<Entity>
- Quella più nuova, andando ad implementare due classi:
- Un ModelIndexerWriterContributor<Entity>
- Un ModelDocumentContributor<Entity>
Nella seconda soluzione i metodi da implementare nelle due classi corrispondono quasi del tutto alla somma dei metodi della prima, per cui al momento le due opzioni sono quasi equivalenti.
In entrambi i casi il metodo principale sarà doGetDocument(Entity entity) nel primo caso e contribute(Document document, Entity entity) nel secondo. Lo scopo di entrambi è quello di aggiungere campi al documento che rappresenta la vostra ricerca, al netto di quelli base già popolati da liferay.
Lascio qui di seguito un esempio del secondo caso
Questo permetterà, dopo il reindex o dopo l’aggiunta di nuovi dati, di popolare l’indice. Tuttavia non abbiamo al momento ancora modo di leggere questi dati.
2. Integrare le custom entity nello strumento di ricerca di Liferay
Uno degli obiettivi potrebbe essere quello di leggere i dati indicizzati e di presentarli nella pagina di ricerca “standard” di Liferay, come se fosse un normale oggetto core.
NOTE
Si ricorda è possibile modificare e personalizzare la pagina di ricerca di Liferay, utilizzando varie portlet collegate, per cui prima di gridare “non funziona!” verificate di non aver attivato qualche filtro, magari sulla tipologia di dato ricercato, che vi blocca la visualizzazione dei vostri oggetti.
NOTA 2
Nel codice che segue verrà oscurato il nome completo dei package, lasciando solamente la parte finale. È inteso che il nome del package deve sempre essere esplicitato per intero.
L’integrazione nella ricerca “nativa” di Liferay è probabilmente la parte più complessa, poiché serve implementare una serie di classi.
Base Searcher
Serve ad abilitare la ricerca per la nostra entity. Si noti che viene chiamato il metodo setPermissionAware(boolean) che indica a Liferay che i risultati tornati dovranno subire il check del layer permissions prima di poter essere visualizzati dall’utente finale.
JAVA
@Component(
property = "model.class.name=*****.page.search.model.PageEntry",
service = BaseSearcher.class
)
public class PageEntrySearcher extends BaseSearcher {
public PageEntrySearcher() {
setDefaultSelectedFieldNames(
Field.ASSET_TAG_NAMES, Field.ASSET_CATEGORY_IDS, Field.COMPANY_ID, Field.ENTRY_CLASS_NAME,
Field.ENTRY_CLASS_PK, Field.CONTENT, Field.MODIFIED_DATE, Field.SUBTITLE,
Field.SCOPE_GROUP_ID, Field.GROUP_ID, Field.TITLE, Field.UID);
setFilterSearch(true);
setPermissionAware(true);
}
@Override
public String getClassName() {
return _CLASS_NAME;
}
private static final String _CLASS_NAME = PageEntry.class.getName();
}
ModelSearchConfigurator
Serve ad indicare a Liferay quali altri servizi devono essere utilizzati per le varie funzionalità.
JAVA
@Component(service = ModelSearchConfigurator.class)
public class PageEntryModelSearchConfigurator
implements ModelSearchConfigurator<PageEntry> {
@Override
public String getClassName() {
return PageEntry.class.getName();
}
@Override
public String[] getDefaultSelectedFieldNames() {
return new String[] {
Field.ASSET_TAG_NAMES, Field.ASSET_CATEGORY_IDS, Field.COMPANY_ID,
Field.ENTRY_CLASS_NAME, Field.ENTRY_CLASS_PK, Field.MODIFIED_DATE,
Field.SCOPE_GROUP_ID, Field.GROUP_ID, Field.UID
};
}
@Override
public String[] getDefaultSelectedLocalizedFieldNames() {
return new String[] {};
}
@Override
public ModelIndexerWriterContributor<PageEntry> getModelIndexerWriterContributor() {
return modelIndexWriterContributor;
}
@Override
public ModelSummaryContributor getModelSummaryContributor() {
return modelSummaryContributor;
}
@Override
public ModelVisibilityContributor getModelVisibilityContributor() {
return _modelVisibilityContributor;
}
@Override
public boolean isPermissionAware() {
return true;
}
@Reference
private PageEntryLocalService _pageEntryLocalService;
@Reference
private DynamicQueryBatchIndexingActionableFactory
_dynamicQueryBatchIndexingActionableFactory;
@Reference
private Localization _localization;
@Reference(target = "(indexer.class.name=*****.page.search.model.PageEntry)")
private ModelIndexerWriterContributor<PageEntry> modelIndexWriterContributor;
@Reference(target = "(indexer.class.name=*****.page.search.model.PageEntry)")
private ModelSummaryContributor modelSummaryContributor;
@Reference(target = "(indexer.class.name=*****.page.search.model.PageEntry)")
private ModelVisibilityContributor _modelVisibilityContributor;
}
In particolare a noi interessano i tre campi finali, poi usati nei rispettivi getter.
Il primo, cioè il ModelIndexerWriterContributor è lo stesso utilizzato al punto 1 di questa soluzione.
ModelVisibilityContributor
Serve a determinare se un dato oggetto è visibile nella ricerca, in base al suo stato (se rilevante)
JAVA
@Component(
property = "indexer.class.name=*****.page.search.model.PageEntry",
service = ModelVisibilityContributor.class
)
public class PageEntryModelVisibilityContributor
implements ModelVisibilityContributor {
@Override
public boolean isVisible(long classPK, int status) {
try {
PageEntry entry = _pageEntryLocalService.fetchPageEntry(classPK);
return isVisible(entry.getStatus(), status);
}
catch (Throwable portalException) {
if (_log.isWarnEnabled()) {
_log.warn(
"Unable to check visibility for page entry ",
portalException);
}
return false;
}
}
private static final Log _log = LogFactoryUtil.getLog(
PageEntryModelVisibilityContributor.class);
@Reference
private PageEntryLocalService _pageEntryLocalService;
}
ModelSummaryContributor
Serve a creare il “summary” della nostra entity, cioè il testo che verrà mostrato nella nostra ricerca per ogni oggetto. Si compone di due campi: titolo e descrizione. In questo caso l’implementazione è abbastanza semplice, viene scelto di mostrare due campi localizzati e di impostare una lunghezza massima della descrizione a 200 caratteri (per motivi legati al design).
JAVA
@Component(
property = "indexer.class.name=**.page.search.model.PageEntry",
service = ModelSummaryContributor.class
)
public class PageEntryModelSummaryContributor
implements ModelSummaryContributor {
@Override
public Summary getSummary(
Document document, Locale locale, String snippet) {
String languageId = LocaleUtil.toLanguageId(locale);
return _createSummary(
document, _localization.getLocalizedName(Field.SUBTITLE, languageId),
_localization.getLocalizedName(Field.TITLE, languageId));
}
private Summary _createSummary(
Document document, String contentField, String titleField) {
Summary summary = new Summary(
document.get(titleField, titleField),
document.get(contentField, contentField));
summary.setMaxContentLength(200);
return summary;
}
@Reference
private Localization _localization;
}
KeywordQueryContributor
L’ultima classe della nostra serie, che serve per specificare a Liferay come costruire la query per i nostri oggetti, in base alla ricerca effettuata dall’utente. Per farla breve, in quali campi cercare.
@Component(
property = "indexer.class.name=*****.page.search.model.PageEntry",
service = KeywordQueryContributor.class
)
public class PageEntryKeywordQueryContributor
implements KeywordQueryContributor {
@Override
public void contribute(
String keywords, BooleanQuery booleanQuery,
KeywordQueryContributorHelper keywordQueryContributorHelper) {
SearchContext searchContext =
keywordQueryContributorHelper.getSearchContext();
_queryHelper.addSearchLocalizedTerm(
booleanQuery, searchContext, Field.CONTENT, false);
_queryHelper.addSearchLocalizedTerm(
booleanQuery, searchContext, Field.SUBTITLE, false);
_queryHelper.addSearchLocalizedTerm(
booleanQuery, searchContext, Field.TITLE, false);
QueryConfig queryConfig = searchContext.getQueryConfig();
queryConfig.addHighlightFieldNames(
_searchLocalizationHelper.getLocalizedFieldNames(
new String[] {Field.SUBTITLE, Field.TITLE}, searchContext));
}
@Reference
private QueryHelper _queryHelper;
@Reference
private SearchLocalizationHelper _searchLocalizationHelper;
}
Come anticipato, non è proprio una implementazione da 2 minuti di lavoro, ma ovviamente il framework è molto articolato, e supporta l’implementazione di più motori di ricerca a runtime, per cui deve necessariamente essere in grado di gestire tutti gli aspetti della ricerca.
Tutto apposto?
No, mi dispiace per voi ma mancano ancora due piccoli passaggi. Finora abbiamo implementato il salvataggio dei dati nell’indice e la loro lettura, manca ovviamente la parte di presentation, cioè come effettivamente mostrare i risultati della nostra ricerca.
BaseAssetRendererFactory
È una classe di utility, gestita da OSGI, che ha il compito essenzialmente di istanziare un AssetRenderer per ogni nostro oggetto e passargli i servizi che servono.
JAVA
@Component(
immediate = true,
property = {"javax.portlet.name=" + PageEntryWebKeys.PageEntryKey},
service = AssetRendererFactory.class)
public class PageEntryAssetRendererFactory
extends BaseAssetRendererFactory<PageEntry> {
public static final String TYPE = "pageEntry";
public PageEntryAssetRendererFactory() {
setClassName(PageEntry.class.getName());
setLinkable(true);
setPortletId(PageEntryWebKeys.PageEntryKey);
setSearchable(true);
}
@Override
public AssetRenderer<PageEntry> getAssetRenderer(long classPK, int type)
throws PortalException {
PageEntryAssetRenderer pageEntryAssetRenderer =
new PageEntryAssetRenderer(
_pageEntryLocalService.getPageEntry(classPK));
pageEntryAssetRenderer.setAssetRendererType(type);
pageEntryAssetRenderer.setServletContext(_servletContext);
pageEntryAssetRenderer.setLayoutLocalService(_layoutLocalService);
return pageEntryAssetRenderer;
}
@Override
public String getType() {
return TYPE;
}
@Override
public String getIconCssClass() {
return "web-content";
}
@Override
public PortletURL getURLView(
LiferayPortletResponse liferayPortletResponse,
WindowState windowState) {
LiferayPortletURL liferayPortletURL =
liferayPortletResponse.createLiferayPortletURL(
PageEntryWebKeys.PageEntryKey, PortletRequest.RENDER_PHASE);
try {
liferayPortletURL.setWindowState(windowState);
}
catch (WindowStateException wse) {
}
return liferayPortletURL;
}
@Reference
private PageEntryLocalService _pageEntryLocalService;
@Reference
private LayoutLocalService _layoutLocalService;
@Reference(target = "(osgi.web.symbolicname=***.page.search.web)", unbind = "-")
private ServletContext _servletContext;
}
BaseJSPAssetRenderer
Questa classe è quella che si occupa effettivamente di definire come il nostro oggetto sarà visualizzato nella ricerca di Liferay, ed in particolare il link al risultato, l’immagine, titolo, summary, status ed eventuali permissions. Come vedete si tratta di un oggetto plain, non gestito da OSGI.
JAVA
public class PageEntryAssetRenderer extends BaseJSPAssetRenderer<PageEntry> {
public PageEntryAssetRenderer(PageEntry pageEntry) {
_pageEntry = pageEntry;
}
@Override
public PageEntry getAssetObject() {
return _pageEntry;
}
@Override
public String getClassName() {
return PageEntry.class.getName();
}
@Override
public long getClassPK() {
return _pageEntry.getPageId();
}
@Override
public long getGroupId() {
return _pageEntry.getGroupId();
}
@Override
public String getJspPath(
HttpServletRequest httpServletRequest, String template) {
return null;
}
@Override
public String getThumbnailPath(PortletRequest portletRequest) throws Exception {
try {
Layout layout = _layoutLocalService.fetchLayout(_pageEntry.getPlid());
if (layout != null && layout.getIconImage()) {
return "/image/logo?img_id="+layout.getIconImageId();
}
} catch (Throwable e) {
_log.error(e.getMessage());
return super.getThumbnailPath(portletRequest);
}
return super.getThumbnailPath(portletRequest);
}
@Override
public boolean include(
HttpServletRequest request, HttpServletResponse response,
String template)
throws Exception {
request.setAttribute(PageEntryWebKeys.PageEntry, _pageEntry);
return super.include(request, response, template);
}
@Override
public String getSummary(
PortletRequest portletRequest, PortletResponse portletResponse) {
return _pageEntry.getSummary(getLocale(portletRequest)) + " " + _pageEntry.getPlid();
}
@Override
public String getTitle(Locale locale) {
return _pageEntry.getTitle(locale);
}
@Override
public int getStatus() {
return _pageEntry.getStatus();
}
@Override
public String getURLViewInContext(
LiferayPortletRequest liferayPortletRequest,
LiferayPortletResponse liferayPortletResponse,
String noSuchEntryRedirect)
throws PortalException {
ThemeDisplay themeDisplay =
(ThemeDisplay)liferayPortletRequest.getAttribute(
WebKeys.THEME_DISPLAY);
return getURLViewInContext(themeDisplay, noSuchEntryRedirect);
}
public String getURLViewInContext(
ThemeDisplay themeDisplay, String noSuchEntryRedirect)
throws PortalException {
try {
Layout layout = _layoutLocalService.fetchLayout(_pageEntry.getPlid());
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(themeDisplay.getScopeGroup().getPathFriendlyURL(layout.isPrivateLayout(), themeDisplay));
urlBuilder.append(themeDisplay.getScopeGroup().getFriendlyURL());
urlBuilder.append(layout.getFriendlyURL(themeDisplay.getLocale()));
return urlBuilder.toString();
} catch (Throwable e) {
_log.error(e.getMessage());
return "/";
}
}
@Override
public long getUserId() {
return _pageEntry.getUserId();
}
@Override
public String getUserName() {
return _pageEntry.getUserName();
}
@Override
public String getUuid() {
return _pageEntry.getUuid();
}
public boolean hasDeletePermission(PermissionChecker permissionChecker) {
return false;
}
@Override
public boolean hasEditPermission(PermissionChecker permissionChecker) {
return false;
}
@Override
public boolean hasViewPermission(PermissionChecker permissionChecker) {
try {
return LayoutPermissionUtil.contains(permissionChecker, _pageEntry.getPlid(), ActionKeys.VIEW);
} catch (PortalException e) {
_log.error(e.getMessage());
if (_log.isDebugEnabled())
_log.debug(e.getMessage(), e);
}
return false;
}
public void setLayoutLocalService(LayoutLocalService layoutLocalService) {
_layoutLocalService = layoutLocalService;
}
private static final Log _log = LogFactoryUtil.getLog(PageEntryAssetRenderer.class);
private PageEntry _pageEntry;
private LayoutLocalService _layoutLocalService;
}
3. Eseguire ricerche custom ed elaborare i risultati
Questo terzo obiettivo esula dal secondo. Ci proponiamo cioè di effettuare query direttamente verso l’indice e di mostrare i risultati esternamente alla ricerca di Liferay, per esempio attraverso una portlet custom o esposti attraverso un servizio. Lo step necessario resta il punto 1 di questa soluzione, cioè la scrittura dei dati nell’indice.
Mi scuserete ma per questo terzo caso il codice sarà leggermente diverso, sostanzialmente perché riferito ad una diversa entity di un diverso progetto.
La logica resta invariata, in ogni caso.
L’idea alla base di questa soluzione è di utilizzare servizi che Liferay mette già a disposizione nel package com.liferay.portal.search per costruire query che a tutti gli effetti assomigliano alla trasposizione Java di una query su Elasticsearch/Opensearch.
Vediamo di quali oggetti parliamo, e poi qualche esempio.
JAVA
@Reference
protected Queries queries;
@Reference
protected Searcher searcher;
@Reference
protected SearchRequestBuilderFactory searchRequestBuilderFactory;
@Reference
protected HighlightBuilderFactory highlightBuilderFactory;
@Reference
protected FieldConfigBuilderFactory fieldConfigBuilderFactory;
@Reference
private Sorts _sorts;
Come già detto, sono tutti oggetti dello stesso package e sono tutti iniettabili tramite OSGI.
Un esempio di ricerca
JAVA
public SearchResultDto doQuery(long userId, Integer page, Integer pageSize, String keywords, boolean enabledContacts, boolean searchAnd, SearchContactDto searchFilter) {
//build base search
SearchRequestBuilder searchRequestBuilder = searchRequestBuilderFactory.builder();
searchRequestBuilder.withSearchContext(
searchContext -> { searchContext.setCompanyId(PortalUtil.getDefaultCompanyId());
searchContext.setEntryClassNames(new String[] {Contatto.class.getName()});
});
BooleanQuery query = queries.booleanQuery();
//add dates filter
query = addValidityFilters(query, enabledContacts);
//add keywords filter
if (Validator.isNotNull(keywords))
query = addFullTextFilter(query, keywords, searchAnd, permissions);
//add columns specific filters
if (searchFilter != null && searchFilter.getSearchFilter() != null && searchFilter.getSearchFilter().getColumnFilters() != null) {
for (ColumnFilterDto colFilter : searchFilter.getSearchFilter().getColumnFilters()) {
query = addNumberFilter(query, colFilter, false, false);
}
}
//add highlights to evidence match to users
Highlight highlight = highlightBuilderFactory.builder() .addFieldConfig(fieldConfigBuilderFactory.builder(RubricaSearchTerms.UFFICIO).fragmentSize(80).numFragments(3).build())
.preTags(StringPool.BLANK)
.postTags(StringPool.BLANK)
.requireFieldMatch(true)
.build();
//build search request, with paging and sorting
SearchRequest searchRequest = searchRequestBuilder
.query(query)
.from(page * pageSize)
.size(pageSize)
.emptySearchEnabled(true)
.highlightEnabled(true)
.highlight(highlight)
.sorts(
_sorts.score(),
_sorts.field(Field.NAME+StringPool.UNDERLINE+Field.SORTABLE_FIELD_SUFFIX, SortOrder.ASC),
_sorts.field(RubricaSearchTerms.UFFICIO+StringPool.UNDERLINE+Field.SORTABLE_FIELD_SUFFIX, SortOrder.ASC)
)
.build();
// execute actual search
SearchResponse searchResponse = searcher.search(searchRequest);
List<ContattoDto> result = new ArrayList<>();
// extract results and convert to usefull dto
for (SearchHit hit : searchResponse.getSearchHits().getSearchHits()) {
if (_log.isDebugEnabled())
_log.debug(hit.getDocument());
result.add(contactMapper.mapToDto(hit));
}
return new SearchResultDto(result, Long.valueOf(searchResponse.getSearchHits().getTotalHits()).intValue());
}
I metodi correlati ritornano sempre delle BooleanQuery, ad esempio
protected BooleanQuery addValidityFilters(BooleanQuery query, boolean enabled) {
Date now = new Date();
if (enabled) {
BooleanQuery dateAfterQuery = queries.booleanQuery();
dateAfterQuery.addShouldQueryClauses(queries.booleanQuery().addMustNotQueryClauses(queries.exists(RubricaSearchTerms.UNTIL)));
dateAfterQuery.addShouldQueryClauses(queries.rangeTerm(RubricaSearchTerms.FROM, true, false, now, null));
BooleanQuery dateBeforeQuery = queries.booleanQuery();
dateBeforeQuery.addShouldQueryClauses(queries.booleanQuery().addMustNotQueryClauses(queries.exists(RubricaSearchTerms.FROM)));
dateBeforeQuery.addShouldQueryClauses(queries.rangeTerm(RubricaSearchTerms.UNTIL, false, true, null, now));
query = query.addFilterQueryClauses(dateAfterQuery).addFilterQueryClauses(dateBeforeQuery);
}
return query;
}
Alcuni metodi interessanti dell’oggetto BooleanQuery
- addMustQueryClauses → aggiunge un filtro “and”
- addMustNotQueryClauses → aggiunge un filtro “and not”
- addShouldQueryClauses → aggiunge il filtro “or”
- booleanQuery → aggiunge una sottoquery
- terms → aggiunge un termine alla query
La combinazione di questi metodi e di sottoquery permette di mappare la complessità delle query.
I Risultati
Il risultato dei punti 2 e 3, entrambi unitamente al punto 1, permettono di visualizzare i dati di nostre entity custom in due modalità: tramite la pagina di ricerca di Liferay e tramite una portlet custom che esegua direttamente query sul motore di indicizzazione.
Le Conclusioni
La combinazione di queste tre implementazione permette di implementare la ricerca a tutti i livelli di entity custom sviluppate ad hoc. Con una logica simile è perfino possibile modificare le logiche di ricerca di oggetti core o di rimappare gli oggetti core in wrapper e indicizzarli a parte rispetto ai rispettivi core.
Nell’esempio ai punti 1 e 2, si trattava infatti di rimappare le pagine di Liferay, con logiche diverse ed aggiungendo dati alla ricerca, tratti da campi expando. Si può infine implementare una ricerca che esuli da quella standard di liferay e venga visualizzata tramite servizi e portlet custom.
Ti è piaciuto quanto hai letto? Iscriviti a MISPECIAL, la nostra newsletter, per ricevere altri interessanti contenuti.
Iscriviti a MISPECIAL