SearchApp.java

  1. /*
  2.  * Copyright (c) 2002-2022, City of Paris
  3.  * All rights reserved.
  4.  *
  5.  * Redistribution and use in source and binary forms, with or without
  6.  * modification, are permitted provided that the following conditions
  7.  * are met:
  8.  *
  9.  *  1. Redistributions of source code must retain the above copyright notice
  10.  *     and the following disclaimer.
  11.  *
  12.  *  2. Redistributions in binary form must reproduce the above copyright notice
  13.  *     and the following disclaimer in the documentation and/or other materials
  14.  *     provided with the distribution.
  15.  *
  16.  *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
  17.  *     contributors may be used to endorse or promote products derived from
  18.  *     this software without specific prior written permission.
  19.  *
  20.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  21.  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  22.  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  23.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
  24.  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  25.  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  26.  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  27.  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  28.  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  29.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  30.  * POSSIBILITY OF SUCH DAMAGE.
  31.  *
  32.  * License 1.0
  33.  */
  34. package fr.paris.lutece.portal.web.search;

  35. import fr.paris.lutece.portal.business.search.SearchParameterHome;
  36. import fr.paris.lutece.portal.service.html.EncodingService;
  37. import fr.paris.lutece.portal.service.i18n.I18nService;
  38. import fr.paris.lutece.portal.service.message.SiteMessage;
  39. import fr.paris.lutece.portal.service.message.SiteMessageException;
  40. import fr.paris.lutece.portal.service.message.SiteMessageService;
  41. import fr.paris.lutece.portal.service.plugin.Plugin;
  42. import fr.paris.lutece.portal.service.search.ISponsoredLinksSearchService;
  43. import fr.paris.lutece.portal.service.search.QueryEvent;
  44. import fr.paris.lutece.portal.service.search.QueryListenersService;
  45. import fr.paris.lutece.portal.service.search.SearchEngine;
  46. import fr.paris.lutece.portal.service.search.SearchResult;
  47. import fr.paris.lutece.portal.service.search.SearchService;
  48. import fr.paris.lutece.portal.service.search.SponsoredLinksSearchService;
  49. import fr.paris.lutece.portal.service.template.AppTemplateService;
  50. import fr.paris.lutece.portal.service.util.AppLogService;
  51. import fr.paris.lutece.portal.service.util.AppPropertiesService;
  52. import fr.paris.lutece.portal.web.xpages.XPage;
  53. import fr.paris.lutece.portal.web.xpages.XPageApplication;
  54. import fr.paris.lutece.util.html.HtmlTemplate;
  55. import fr.paris.lutece.util.html.Paginator;
  56. import fr.paris.lutece.util.http.SecurityUtil;
  57. import fr.paris.lutece.util.url.UrlItem;

  58. import org.apache.commons.lang3.StringUtils;

  59. import java.io.UnsupportedEncodingException;
  60. import java.nio.charset.StandardCharsets;
  61. import java.util.HashMap;
  62. import java.util.List;
  63. import java.util.Locale;
  64. import java.util.Map;
  65. import java.util.Optional;

  66. import javax.inject.Inject;
  67. import javax.inject.Named;

  68. import javax.servlet.http.HttpServletRequest;

  69. /**
  70.  * This class provides search results pages.
  71.  */
  72. public class SearchApp implements XPageApplication
  73. {
  74.     /**
  75.      * Serial version UID
  76.      */
  77.     private static final long serialVersionUID = -9101183157272256639L;

  78.     // //////////////////////////////////////////////////////////////////////////
  79.     // Constants
  80.     private static final String BEAN_SEARCH_ENGINE = "searchEngine";
  81.     private static final String TEMPLATE_RESULTS = "skin/search/search_results.html";
  82.     private static final String PROPERTY_SEARCH_PAGE_URL = "search.pageSearch.baseUrl";
  83.     private static final String PROPERTY_RESULTS_PER_PAGE = "search.nb.docs.per.page";
  84.     private static final String PROPERTY_PATH_LABEL = "portal.search.search_results.pathLabel";
  85.     private static final String PROPERTY_PAGE_TITLE = "portal.search.search_results.pageTitle";
  86.     private static final String MESSAGE_INVALID_SEARCH_TERMS = "portal.search.message.invalidSearchTerms";
  87.     private static final String MESSAGE_ENCODING_ERROR = "portal.search.message.encodingError";
  88.     private static final String DEFAULT_RESULTS_PER_PAGE = "10";
  89.     private static final String DEFAULT_PAGE_INDEX = "1";
  90.     private static final String PARAMETER_PAGE_INDEX = "page_index";
  91.     private static final String PARAMETER_NB_ITEMS_PER_PAGE = "items_per_page";
  92.     private static final String PARAMETER_QUERY = "query";
  93.     private static final String PARAMETER_TAG_FILTER = "tag_filter";
  94.     private static final String PARAMETER_DEFAULT_OPERATOR = "default_operator";
  95.     private static final String MARK_RESULTS_LIST = "results_list";
  96.     private static final String MARK_QUERY = "query";
  97.     private static final String MARK_PAGINATOR = "paginator";
  98.     private static final String MARK_NB_ITEMS_PER_PAGE = "nb_items_per_page";
  99.     private static final String MARK_ERROR = "error";
  100.     private static final String MARK_SPONSOREDLINKS_SET = "sponsoredlinks_set";
  101.     private static final String MARK_LIST_TYPE_AND_LINK = "list_type_and_link";
  102.     private static final String PROPERTY_ENCODE_URI = "search.encode.uri";
  103.     private static final String PROPERTY_ENCODE_URI_ENCODING = "search.encode.uri.encoding";
  104.     private static final String CONSTANT_HTTP_METHOD_GET = "GET";
  105.     private static final boolean DEFAULT_ENCODE_URI = false;
  106.     @Inject
  107.     @Named( BEAN_SEARCH_ENGINE )
  108.     private SearchEngine _engine;

  109.     /**
  110.      * Returns search results
  111.      *
  112.      * @param request
  113.      *            The HTTP request.
  114.      * @param nMode
  115.      *            The current mode.
  116.      * @param plugin
  117.      *            The plugin
  118.      * @return The HTML code of the page.
  119.      * @throws SiteMessageException
  120.      *             If an error occurs
  121.      */
  122.     @Override
  123.     public XPage getPage( HttpServletRequest request, int nMode, Plugin plugin ) throws SiteMessageException
  124.     {
  125.         XPage page = new XPage( );
  126.         String strQuery = request.getParameter( PARAMETER_QUERY );
  127.         String strTagFilter = request.getParameter( PARAMETER_TAG_FILTER );

  128.         String strEncoding = AppPropertiesService.getProperty( PROPERTY_ENCODE_URI_ENCODING, StandardCharsets.ISO_8859_1.name( ) );

  129.         if ( StringUtils.equalsIgnoreCase( CONSTANT_HTTP_METHOD_GET, request.getMethod( ) )
  130.                 && !StringUtils.equalsIgnoreCase( strEncoding, StandardCharsets.UTF_8.name( ) ) )
  131.         {
  132.             try
  133.             {
  134.                 if ( StringUtils.isNotBlank( strQuery ) )
  135.                 {
  136.                     strQuery = new String( strQuery.getBytes( strEncoding ), StandardCharsets.UTF_8 );
  137.                 }

  138.                 if ( StringUtils.isNotBlank( strTagFilter ) )
  139.                 {
  140.                     strTagFilter = new String( strTagFilter.getBytes( strEncoding ), StandardCharsets.UTF_8 );
  141.                 }
  142.             }
  143.             catch( UnsupportedEncodingException e )
  144.             {
  145.                 AppLogService.error( e.getMessage( ), e );
  146.             }
  147.         }

  148.         if ( StringUtils.isNotEmpty( strTagFilter ) )
  149.         {
  150.             strQuery = strTagFilter;
  151.         }

  152.         boolean bEncodeUri = Boolean.parseBoolean( AppPropertiesService.getProperty( PROPERTY_ENCODE_URI, Boolean.toString( DEFAULT_ENCODE_URI ) ) );

  153.         String strSearchPageUrl = AppPropertiesService.getProperty( PROPERTY_SEARCH_PAGE_URL );
  154.         String strError = "";
  155.         Locale locale = request.getLocale( );

  156.         // Check XSS characters
  157.         if ( ( strQuery != null ) && ( SecurityUtil.containsXssCharacters( request, strQuery ) ) )
  158.         {
  159.             strError = I18nService.getLocalizedString( MESSAGE_INVALID_SEARCH_TERMS, locale );
  160.             strQuery = "";
  161.         }

  162.         String strDefaultNbItemPerPage = AppPropertiesService.getProperty( PROPERTY_RESULTS_PER_PAGE, DEFAULT_RESULTS_PER_PAGE );
  163.         String strNbItemPerPage = Optional.ofNullable( request.getParameter( PARAMETER_NB_ITEMS_PER_PAGE ) ).orElse( strDefaultNbItemPerPage );
  164.         int nNbItemsPerPage = Integer.parseInt( strNbItemPerPage );

  165.         String strCurrentPageIndex = Optional.ofNullable( request.getParameter( PARAMETER_PAGE_INDEX ) ).orElse( DEFAULT_PAGE_INDEX );

  166.         List<SearchResult> listResults = _engine.getSearchResults( strQuery, request );

  167.         // The page should not be added to the cache

  168.         // Notify results infos to QueryEventListeners
  169.         notifyQueryListeners( strQuery, listResults.size( ), request );

  170.         UrlItem url = new UrlItem( strSearchPageUrl );
  171.         String strQueryForPaginator = strQuery;

  172.         if ( bEncodeUri )
  173.         {
  174.             strQueryForPaginator = encodeUrl( request, strQuery );
  175.         }

  176.         if ( StringUtils.isNotBlank( strTagFilter ) )
  177.         {
  178.             strQuery = "";
  179.         }

  180.         url.addParameter( PARAMETER_QUERY, strQueryForPaginator );
  181.         url.addParameter( PARAMETER_NB_ITEMS_PER_PAGE, nNbItemsPerPage );

  182.         StringBuilder sbUrl = new StringBuilder( );
  183.         sbUrl = sbUrl.append( url.getUrl( ) );

  184.         Map<String, Object> model = new HashMap<>( );
  185.         if ( StringUtils.isNotBlank( request.getParameter( PARAMETER_DEFAULT_OPERATOR ) ) )
  186.         {
  187.             sbUrl = sbUrl.append( "&default_operator=" + request.getParameter( PARAMETER_DEFAULT_OPERATOR ) );
  188.             // Override default_operator value
  189.             model.put( PARAMETER_DEFAULT_OPERATOR, request.getParameter( PARAMETER_DEFAULT_OPERATOR ) );
  190.         }

  191.         Paginator<SearchResult> paginator = new Paginator<>( listResults, nNbItemsPerPage, sbUrl.toString( ), PARAMETER_PAGE_INDEX, strCurrentPageIndex );

  192.         model.put( MARK_RESULTS_LIST, paginator.getPageItems( ) );
  193.         model.put( MARK_QUERY, strQuery );
  194.         model.put( MARK_PAGINATOR, paginator );
  195.         model.put( MARK_NB_ITEMS_PER_PAGE, strNbItemPerPage );
  196.         model.put( MARK_ERROR, strError );

  197.         ISponsoredLinksSearchService sponsoredLinksService = new SponsoredLinksSearchService( );

  198.         if ( sponsoredLinksService.isAvailable( ) )
  199.         {
  200.             model.put( MARK_SPONSOREDLINKS_SET, sponsoredLinksService.getHtmlCode( strQuery, locale ) );
  201.         }

  202.         model.put( MARK_LIST_TYPE_AND_LINK, SearchService.getSearchTypesAndLinks( ) );
  203.         model.putAll( SearchParameterHome.findAll( ) );

  204.         HtmlTemplate template = AppTemplateService.getTemplate( TEMPLATE_RESULTS, locale, model );
  205.         page.setPathLabel( I18nService.getLocalizedString( PROPERTY_PATH_LABEL, locale ) );
  206.         page.setTitle( I18nService.getLocalizedString( PROPERTY_PAGE_TITLE, locale ) );
  207.         page.setContent( template.getHtml( ) );

  208.         return page;
  209.     }

  210.     /**
  211.      * Encode an url string
  212.      *
  213.      * @param request
  214.      *            The HTTP request
  215.      * @param strSource
  216.      *            The string to encode
  217.      * @return The encoded string
  218.      * @throws SiteMessageException
  219.      *             If an error occurs
  220.      */
  221.     public static String encodeUrl( HttpServletRequest request, String strSource ) throws SiteMessageException
  222.     {
  223.         String strSourceUrl = ( strSource != null ) ? strSource : StringUtils.EMPTY;

  224.         String strEncoded = EncodingService.encodeUrl( strSourceUrl, PROPERTY_ENCODE_URI_ENCODING, StandardCharsets.ISO_8859_1.name( ) );

  225.         if ( StringUtils.isBlank( strEncoded ) && StringUtils.isNotBlank( strSourceUrl ) )
  226.         {
  227.             SiteMessageService.setMessage( request, MESSAGE_ENCODING_ERROR, SiteMessage.TYPE_ERROR );
  228.         }

  229.         return strEncoded;
  230.     }

  231.     /**
  232.      * Notify all query Listeners
  233.      *
  234.      * @param strQuery
  235.      *            The query
  236.      * @param nResultsCount
  237.      *            The results count
  238.      * @param request
  239.      *            The request
  240.      */
  241.     private void notifyQueryListeners( String strQuery, int nResultsCount, HttpServletRequest request )
  242.     {
  243.         QueryEvent event = new QueryEvent( );
  244.         event.setQuery( strQuery );
  245.         event.setResultsCount( nResultsCount );
  246.         event.setRequest( request );
  247.         QueryListenersService.getInstance( ).notifyListeners( event );
  248.     }
  249. }