SolrSearchApp.java
/*
* Copyright (c) 2002-2021, City of Paris
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice
* and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice
* and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* License 1.0
*/
package fr.paris.lutece.plugins.search.solr.web;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.solr.client.solrj.response.SpellCheckResponse;
import fr.paris.lutece.plugins.leaflet.business.GeolocItem;
import fr.paris.lutece.plugins.leaflet.service.IconService;
import fr.paris.lutece.plugins.search.solr.business.SolrFacetedResult;
import fr.paris.lutece.plugins.search.solr.business.SolrSearchAppConf;
import fr.paris.lutece.plugins.search.solr.business.SolrSearchEngine;
import fr.paris.lutece.plugins.search.solr.business.SolrSearchResult;
import fr.paris.lutece.plugins.search.solr.business.field.Field;
import fr.paris.lutece.plugins.search.solr.business.field.SolrFieldManager;
import fr.paris.lutece.plugins.search.solr.indexer.SolrItem;
import fr.paris.lutece.plugins.search.solr.service.ISolrSearchAppAddOn;
import fr.paris.lutece.plugins.search.solr.service.SolrSearchAppConfService;
import fr.paris.lutece.plugins.search.solr.util.SolrConstants;
import fr.paris.lutece.plugins.search.solr.util.SolrUtil;
import fr.paris.lutece.portal.service.i18n.I18nService;
import fr.paris.lutece.portal.service.message.SiteMessageException;
import fr.paris.lutece.portal.service.plugin.Plugin;
import fr.paris.lutece.portal.service.search.QueryEvent;
import fr.paris.lutece.portal.service.search.QueryListenersService;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.template.AppTemplateService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.portal.web.xpages.XPage;
import fr.paris.lutece.portal.web.xpages.XPageApplication;
import fr.paris.lutece.util.html.AbstractPaginator;
import fr.paris.lutece.util.html.DelegatePaginator;
import fr.paris.lutece.util.html.HtmlTemplate;
import fr.paris.lutece.util.html.IPaginator;
import fr.paris.lutece.util.string.StringUtil;
import fr.paris.lutece.util.url.UrlItem;
/**
* This page shows some features of Solr like Highlights or Facets.
*
*/
public class SolrSearchApp implements XPageApplication
{
private static final long serialVersionUID = -2504409688612219166L;
private static final String FULL_URL = "fullUrl";
private static final String SOLR_FACET_DATE_GAP = "facetDateGap";
private static final String ALL_SEARCH_QUERY = "*:*";
////////////////////////////////////////////////////////////////////////////
// Constants
private static final String PROPERTY_SEARCH_PAGE_URL = "solr.pageSearch.baseUrl";
private static final String PROPERTY_RESULTS_PER_PAGE = "search.nb.docs.per.page";
private static final String PROPERTY_PATH_LABEL = "portal.search.search_results.pathLabel";
private static final String PROPERTY_PAGE_TITLE = "portal.search.search_results.pageTitle";
private static final String PROPERTY_ONLY_FACTES = "solr.onlyFacets";
private static final String PROPERTY_SOLR_RESPONSE_MAX = "solr.reponse.max";
private static final int SOLR_RESPONSE_MAX = Integer.parseInt( AppPropertiesService.getProperty( PROPERTY_SOLR_RESPONSE_MAX, "50" ) );
private static final String MESSAGE_INVALID_SEARCH_TERMS = "portal.search.message.invalidSearchTerms";
private static final int DEFAULT_RESULTS_PER_PAGE = 10;
private static final String DEFAULT_PAGE_INDEX = "1";
private static final String PARAMETER_PAGE_INDEX = "page_index";
private static final String PARAMETER_NB_ITEMS_PER_PAGE = "items_per_page";
private static final String PARAMETER_QUERY = "query";
public static final String PARAMETER_CONF = "conf";
private static final String PARAMETER_FACET_QUERY = "fq";
private static final String PARAMETER_FACET_LABEL = "facetlabel";
private static final String PARAMETER_FACET_NAME = "facetname";
private static final String PARAMETER_SORT_NAME = "sort_name";
private static final String PARAMETER_SORT_ORDER = "sort_order";
private static final String MARK_RESULTS_LIST = "results_list";
private static final String MARK_QUERY = "query";
private static final String MARK_FACET_QUERY = "facetquery";
private static final String MARK_PAGINATOR = "paginator";
private static final String MARK_NB_ITEMS_PER_PAGE = "nb_items_per_page";
private static final String MARK_ERROR = "error";
private static final String MARK_FACETS = "facets";
private static final String MARK_SOLR_FIELDS = "solr_fields";
private static final String MARK_FACETS_DATE = "facets_date";
private static final String MARK_HISTORIQUE = "historique";
private static final String MARK_SUGGESTION = "suggestion";
private static final String MARK_SORT_NAME = "sort_name";
private static final String MARK_SORT_ORDER = "sort_order";
private static final String MARK_SORT_LIST = "sort_list";
private static final String MARK_FACET_TREE = "facet_tree";
private static final String MARK_FACETS_LIST = "facets_list";
private static final String MARK_ENCODING = "encoding";
private static final String MARK_CONF_QUERY = "conf_user_query";
private static final String MARK_CONF = "conf";
private static final String MARK_POINTS = "points";
private static final String MARK_POINTS_GEOJSON = "geojson";
private static final String MARK_POINTS_ID = "id";
private static final String MARK_POINTS_FIELDCODE = "code";
private static final String MARK_POINTS_TYPE = "type";
private static final String PROPERTY_ENCODE_URI = "search.encode.uri";
private static final boolean DEFAULT_ENCODE_URI = false;
private static final boolean SOLR_SPELLCHECK = AppPropertiesService.getPropertyBoolean( "solr.spellchecker", false );
/**
* Returns search results
*
* @param request
* The HTTP request.
* @param nMode
* The current mode.
* @param plugin
* The plugin
* @return The HTML code of the page.
* @throws SiteMessageException
* exception
*/
@Override
public XPage getPage( HttpServletRequest request, int nMode, Plugin plugin ) throws SiteMessageException
{
XPage page = new XPage( );
String strConfCode = request.getParameter( PARAMETER_CONF );
SolrSearchAppConf conf = SolrSearchAppConfService.loadConfiguration( strConfCode );
if ( conf == null )
{
// Use default conf if the requested one doesn't exist
conf = SolrSearchAppConfService.loadConfiguration( null );
}
Map<String, Object> model = getSearchResultModel( request, conf );
for ( String beanName : conf.getAddonBeanNames( ) )
{
ISolrSearchAppAddOn solrSearchAppAddon = SpringContextService.getBean( beanName );
solrSearchAppAddon.buildPageAddOn( model, request );
}
HtmlTemplate template = AppTemplateService.getTemplate( conf.getTemplate( ), request.getLocale( ), model );
page.setPathLabel( I18nService.getLocalizedString( PROPERTY_PATH_LABEL, request.getLocale( ) ) );
page.setTitle( I18nService.getLocalizedString( PROPERTY_PAGE_TITLE, request.getLocale( ) ) );
page.setContent( template.getHtml( ) );
return page;
}
/**
* @param facetQuery
* @return
*/
private static String getFacetNameFromIHM( String facetQuery )
{
String strFacet = null;
String [ ] myValues = facetQuery.split( ":", 2 );
if ( myValues != null && myValues.length == 2 )
{
strFacet = myValues [0];
}
return strFacet;
}
/**
* @param facetQuery
* @return
*/
private static String getFacetValueFromIHM( String facetQuery )
{
String strFacet = null;
String [ ] myValues = facetQuery.split( ":", 2 );
if ( myValues != null && myValues.length == 2 )
{
strFacet = myValues [1];
}
return strFacet;
}
/**
* Performs a search and fills the model (useful when a page needs to remind search parameters/results) with the default conf
*
* @param request
* the request
* @return the model
* @throws SiteMessageException
* if an error occurs
*/
public static Map<String, Object> getSearchResultModel( HttpServletRequest request ) throws SiteMessageException
{
return getSearchResultModel( request, null );
}
/**
* Performs a search and fills the model (useful when a page needs to remind search parameters/results)
*
* @param request
* the request
* @param conf
* the configuration
* @return the model
* @throws SiteMessageException
* if an error occurs
*/
public static Map<String, Object> getSearchResultModel( HttpServletRequest request, SolrSearchAppConf conf ) throws SiteMessageException
{
String strQuery = request.getParameter( PARAMETER_QUERY );
String [ ] facetQuery = request.getParameterValues( PARAMETER_FACET_QUERY );
String sort = request.getParameter( PARAMETER_SORT_NAME );
String order = request.getParameter( PARAMETER_SORT_ORDER );
String strCurrentPageIndex = request.getParameter( PARAMETER_PAGE_INDEX );
String fname = StringUtils.isBlank( request.getParameter( PARAMETER_FACET_NAME ) ) ? null : request.getParameter( PARAMETER_FACET_LABEL ).trim( );
String flabel = StringUtils.isBlank( request.getParameter( PARAMETER_FACET_LABEL ) ) ? null : request.getParameter( PARAMETER_FACET_LABEL ).trim( );
String strConfCode = request.getParameter( PARAMETER_CONF );
Locale locale = request.getLocale( );
if ( conf == null )
{
// Use default conf if not provided
conf = SolrSearchAppConfService.loadConfiguration( null );
}
StringBuilder sbFacetQueryUrl = new StringBuilder( );
SolrFieldManager sfm = new SolrFieldManager( );
List<String> lstSingleFacetQueries = new ArrayList<>( );
Map<String, Boolean> switchType = getSwitched( );
ArrayList<String> facetQueryTmp = new ArrayList<>( );
if ( facetQuery != null )
{
for ( String fq : facetQuery )
{
if ( sbFacetQueryUrl.indexOf( fq ) == -1 )
{
String strFqNameIHM = getFacetNameFromIHM( fq );
String strFqValueIHM = getFacetValueFromIHM( fq );
if ( fname == null || !switchType.containsKey( fname ) || ( strFqNameIHM != null && strFqValueIHM != null
&& strFqValueIHM.equalsIgnoreCase( flabel ) && strFqNameIHM.equalsIgnoreCase( fname ) ) )
{
sbFacetQueryUrl.append( "&fq=" + fq );
sfm.addFacet( fq );
facetQueryTmp.add( fq );
lstSingleFacetQueries.add( fq );
}
}
}
}
facetQuery = new String [ facetQueryTmp.size( )];
facetQuery = facetQueryTmp.toArray( facetQuery );
if ( StringUtils.isNotBlank( conf.getFilterQuery( ) ) )
{
int nNewLength = ( facetQuery == null ) ? 1 : ( facetQuery.length + 1 );
String [ ] newFacetQuery = Arrays.copyOf( facetQuery, nNewLength );
newFacetQuery [newFacetQuery.length - 1] = conf.getFilterQuery( );
facetQuery = newFacetQuery;
}
boolean bEncodeUri = Boolean.parseBoolean( AppPropertiesService.getProperty( PROPERTY_ENCODE_URI, Boolean.toString( DEFAULT_ENCODE_URI ) ) );
String strSearchPageUrl = AppPropertiesService.getProperty( PROPERTY_SEARCH_PAGE_URL );
String strError = SolrConstants.CONSTANT_EMPTY_STRING;
int nLimit = SOLR_RESPONSE_MAX;
// Check XSS characters
if ( ( strQuery != null ) && ( StringUtil.containsXssCharacters( strQuery ) ) )
{
strError = I18nService.getLocalizedString( MESSAGE_INVALID_SEARCH_TERMS, locale );
}
if ( StringUtils.isNotBlank( strError ) || StringUtils.isBlank( strQuery ) )
{
strQuery = ALL_SEARCH_QUERY;
String strOnlyFacets = AppPropertiesService.getProperty( PROPERTY_ONLY_FACTES );
if ( StringUtils.isNotBlank( strError )
|| ( ArrayUtils.isEmpty( facetQuery ) && StringUtils.isNotBlank( strOnlyFacets ) && SolrConstants.CONSTANT_TRUE.equals( strOnlyFacets ) ) )
{
// no request and no facet selected : we show the facets but no result
nLimit = 0;
}
}
// paginator & session related elements
int nDefaultItemsPerPage = AppPropertiesService.getPropertyInt( PROPERTY_RESULTS_PER_PAGE, DEFAULT_RESULTS_PER_PAGE );
String strCurrentItemsPerPage = request.getParameter( PARAMETER_NB_ITEMS_PER_PAGE );
int nCurrentItemsPerPage = strCurrentItemsPerPage != null ? Integer.parseInt( strCurrentItemsPerPage ) : 0;
int nItemsPerPage = AbstractPaginator.getItemsPerPage( request, AbstractPaginator.PARAMETER_ITEMS_PER_PAGE, nCurrentItemsPerPage,
nDefaultItemsPerPage );
strCurrentPageIndex = ( strCurrentPageIndex != null ) ? strCurrentPageIndex : DEFAULT_PAGE_INDEX;
SolrSearchEngine engine = SolrSearchEngine.getInstance( );
SolrFacetedResult facetedResult = engine.getFacetedSearchResults( strQuery, new String [] {conf.getFieldList()}, facetQuery, sort, order, nLimit, Integer.parseInt( strCurrentPageIndex ),
nItemsPerPage, SOLR_SPELLCHECK );
List<SolrSearchResult> listResults = facetedResult.getSolrSearchResults( );
List<HashMap<String, Object>> points = null;
if ( conf.getExtraMappingQuery( ) )
{
List<SolrSearchResult> listResultsGeoloc = engine.getGeolocSearchResults( strQuery, facetQuery, nLimit );
points = getGeolocModel( listResultsGeoloc );
}
// The page should not be added to the cache
// Notify results infos to QueryEventListeners
notifyQueryListeners( strQuery, listResults.size( ), request );
UrlItem url = new UrlItem( strSearchPageUrl );
String strQueryForPaginator = strQuery;
if ( bEncodeUri )
{
strQueryForPaginator = SolrUtil.encodeUrl( request, strQuery );
}
url.addParameter( PARAMETER_QUERY, strQueryForPaginator );
url.addParameter( PARAMETER_NB_ITEMS_PER_PAGE, nItemsPerPage );
if ( strConfCode != null )
{
url.addParameter( PARAMETER_CONF, strConfCode );
}
for ( String strFacetName : lstSingleFacetQueries )
{
url.addParameter( PARAMETER_FACET_QUERY, SolrUtil.encodeUrl( strFacetName ) );
}
// nb items per page
IPaginator<SolrSearchResult> paginator = new DelegatePaginator<>( listResults, nItemsPerPage, url.getUrl( ), PARAMETER_PAGE_INDEX, strCurrentPageIndex,
facetedResult.getCount( ) );
Map<String, Object> model = new HashMap<>( );
model.put( MARK_RESULTS_LIST, paginator.getPageItems( ) );
// put the query only if it's not *.*
model.put( MARK_QUERY, ALL_SEARCH_QUERY.equals( strQuery ) ? SolrConstants.CONSTANT_EMPTY_STRING : strQuery );
model.put( MARK_FACET_QUERY, sbFacetQueryUrl.toString( ) );
model.put( MARK_PAGINATOR, paginator );
model.put( MARK_NB_ITEMS_PER_PAGE, nItemsPerPage );
model.put( MARK_ERROR, strError );
model.put( MARK_FACETS, facetedResult.getFacetFields( ) );
model.put( MARK_SOLR_FIELDS, SolrFieldManager.getFacetList( ) );
model.put( MARK_FACETS_DATE, facetedResult.getFacetDateList( ) );
model.put( MARK_HISTORIQUE, sfm.getCurrentFacet( ) );
model.put( MARK_FACETS_LIST, lstSingleFacetQueries );
model.put( MARK_CONF_QUERY, strConfCode );
model.put( MARK_CONF, conf );
model.put( MARK_POINTS, points );
if ( SOLR_SPELLCHECK && ( strQuery != null ) && ( strQuery.compareToIgnoreCase( ALL_SEARCH_QUERY ) != 0 ) )
{
SpellCheckResponse checkResponse = engine.getSpellChecker( strQuery );
if ( checkResponse != null )
{
model.put( MARK_SUGGESTION, checkResponse.getCollatedResults( ) );
}
}
model.put( MARK_SORT_NAME, sort );
model.put( MARK_SORT_ORDER, order );
model.put( MARK_SORT_LIST, SolrFieldManager.getSortList( ) );
model.put( MARK_FACET_TREE, facetedResult.getFacetIntersection( ) );
model.put( MARK_ENCODING, SolrUtil.getEncoding( ) );
String strRequestUrl = request.getRequestURL( ).toString( );
model.put( FULL_URL, strRequestUrl );
model.put( SOLR_FACET_DATE_GAP, SolrSearchEngine.SOLR_FACET_DATE_GAP );
return model;
}
private static Map<String, Boolean> getSwitched( )
{
Map<String, Boolean> tabFromSwitch = new HashMap<>( );
for ( Field tmpField : SolrFieldManager.getFacetList( ).values( ) )
{
if ( tmpField.getEnableFacet( ) && "SWITCH".equalsIgnoreCase( tmpField.getOperator( ) ) )
{
tabFromSwitch.put( tmpField.getName( ), Boolean.FALSE );
}
}
return tabFromSwitch;
}
/**
* Returns a model with points data from a geoloc search
*
* @param listResultsGeoloc
* the result of a search
* @return the model
*/
private static List<HashMap<String, Object>> getGeolocModel( List<SolrSearchResult> listResultsGeoloc )
{
List<HashMap<String, Object>> points = new ArrayList<>( listResultsGeoloc.size( ) );
Map<String, String> iconKeysCache = new HashMap<>( );
for ( SolrSearchResult result : listResultsGeoloc )
{
Map<String, Object> dynamicFields = result.getDynamicFields( );
for ( Entry<String, Object> entry : dynamicFields.entrySet( ) )
{
if ( !entry.getKey( ).endsWith( SolrItem.DYNAMIC_GEOJSON_FIELD_SUFFIX ) )
{
continue;
}
HashMap<String, Object> h = new HashMap<>( );
String strJson = (String) entry.getValue( );
GeolocItem geolocItem = null;
try
{
geolocItem = GeolocItem.fromJSON( strJson );
}
catch( IOException e )
{
AppLogService.error( "SolrSearchApp: error parsing geoloc JSON: " + strJson + ", exception " + e );
}
if ( geolocItem != null )
{
String strType = result.getId( ).substring( result.getId( ).lastIndexOf( '_' ) + 1 );
String strIcon;
if ( iconKeysCache.containsKey( geolocItem.getIcon( ) ) )
{
strIcon = iconKeysCache.get( geolocItem.getIcon( ) );
}
else
{
strIcon = IconService.getIcon( strType, geolocItem.getIcon( ) );
iconKeysCache.put( geolocItem.getIcon( ), strIcon );
}
geolocItem.setIcon( strIcon );
h.put( MARK_POINTS_GEOJSON, geolocItem.toJSON( ) );
h.put( MARK_POINTS_ID, result.getId( ).substring( result.getId( ).indexOf( '_' ) + 1, result.getId( ).lastIndexOf( '_' ) ) );
h.put( MARK_POINTS_FIELDCODE, entry.getKey( ).substring( 0, entry.getKey( ).lastIndexOf( '_' ) ) );
h.put( MARK_POINTS_TYPE, strType );
points.add( h );
}
}
}
return points;
}
/**
* Notify all query Listeners
*
* @param strQuery
* The query
* @param nResultsCount
* The results count
* @param request
* The request
*/
private static void notifyQueryListeners( String strQuery, int nResultsCount, HttpServletRequest request )
{
QueryEvent event = new QueryEvent( );
event.setQuery( strQuery );
event.setResultsCount( nResultsCount );
event.setRequest( request );
QueryListenersService.getInstance( ).notifyListeners( event );
}
}