View Javadoc
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  
36  import fr.paris.lutece.portal.business.search.SearchParameterHome;
37  import fr.paris.lutece.portal.service.html.EncodingService;
38  import fr.paris.lutece.portal.service.i18n.I18nService;
39  import fr.paris.lutece.portal.service.message.SiteMessage;
40  import fr.paris.lutece.portal.service.message.SiteMessageException;
41  import fr.paris.lutece.portal.service.message.SiteMessageService;
42  import fr.paris.lutece.portal.service.plugin.Plugin;
43  import fr.paris.lutece.portal.service.search.ISponsoredLinksSearchService;
44  import fr.paris.lutece.portal.service.search.QueryEvent;
45  import fr.paris.lutece.portal.service.search.QueryListenersService;
46  import fr.paris.lutece.portal.service.search.SearchEngine;
47  import fr.paris.lutece.portal.service.search.SearchResult;
48  import fr.paris.lutece.portal.service.search.SearchService;
49  import fr.paris.lutece.portal.service.search.SponsoredLinksSearchService;
50  import fr.paris.lutece.portal.service.template.AppTemplateService;
51  import fr.paris.lutece.portal.service.util.AppLogService;
52  import fr.paris.lutece.portal.service.util.AppPropertiesService;
53  import fr.paris.lutece.portal.web.xpages.XPage;
54  import fr.paris.lutece.portal.web.xpages.XPageApplication;
55  import fr.paris.lutece.util.html.HtmlTemplate;
56  import fr.paris.lutece.util.html.Paginator;
57  import fr.paris.lutece.util.http.SecurityUtil;
58  import fr.paris.lutece.util.url.UrlItem;
59  
60  import org.apache.commons.lang3.StringUtils;
61  
62  import java.io.UnsupportedEncodingException;
63  import java.nio.charset.StandardCharsets;
64  import java.util.HashMap;
65  import java.util.List;
66  import java.util.Locale;
67  import java.util.Map;
68  import java.util.Optional;
69  
70  import javax.inject.Inject;
71  import javax.inject.Named;
72  
73  import javax.servlet.http.HttpServletRequest;
74  
75  /**
76   * This class provides search results pages.
77   */
78  public class SearchApp implements XPageApplication
79  {
80      /**
81       * Serial version UID
82       */
83      private static final long serialVersionUID = -9101183157272256639L;
84  
85      // //////////////////////////////////////////////////////////////////////////
86      // Constants
87      private static final String BEAN_SEARCH_ENGINE = "searchEngine";
88      private static final String TEMPLATE_RESULTS = "skin/search/search_results.html";
89      private static final String PROPERTY_SEARCH_PAGE_URL = "search.pageSearch.baseUrl";
90      private static final String PROPERTY_RESULTS_PER_PAGE = "search.nb.docs.per.page";
91      private static final String PROPERTY_PATH_LABEL = "portal.search.search_results.pathLabel";
92      private static final String PROPERTY_PAGE_TITLE = "portal.search.search_results.pageTitle";
93      private static final String MESSAGE_INVALID_SEARCH_TERMS = "portal.search.message.invalidSearchTerms";
94      private static final String MESSAGE_ENCODING_ERROR = "portal.search.message.encodingError";
95      private static final String DEFAULT_RESULTS_PER_PAGE = "10";
96      private static final String DEFAULT_PAGE_INDEX = "1";
97      private static final String PARAMETER_PAGE_INDEX = "page_index";
98      private static final String PARAMETER_NB_ITEMS_PER_PAGE = "items_per_page";
99      private static final String PARAMETER_QUERY = "query";
100     private static final String PARAMETER_TAG_FILTER = "tag_filter";
101     private static final String PARAMETER_DEFAULT_OPERATOR = "default_operator";
102     private static final String MARK_RESULTS_LIST = "results_list";
103     private static final String MARK_QUERY = "query";
104     private static final String MARK_PAGINATOR = "paginator";
105     private static final String MARK_NB_ITEMS_PER_PAGE = "nb_items_per_page";
106     private static final String MARK_ERROR = "error";
107     private static final String MARK_SPONSOREDLINKS_SET = "sponsoredlinks_set";
108     private static final String MARK_LIST_TYPE_AND_LINK = "list_type_and_link";
109     private static final String PROPERTY_ENCODE_URI = "search.encode.uri";
110     private static final String PROPERTY_ENCODE_URI_ENCODING = "search.encode.uri.encoding";
111     private static final String CONSTANT_HTTP_METHOD_GET = "GET";
112     private static final boolean DEFAULT_ENCODE_URI = false;
113     @Inject
114     @Named( BEAN_SEARCH_ENGINE )
115     private SearchEngine _engine;
116 
117     /**
118      * Returns search results
119      *
120      * @param request
121      *            The HTTP request.
122      * @param nMode
123      *            The current mode.
124      * @param plugin
125      *            The plugin
126      * @return The HTML code of the page.
127      * @throws SiteMessageException
128      *             If an error occurs
129      */
130     @Override
131     public XPage getPage( HttpServletRequest request, int nMode, Plugin plugin ) throws SiteMessageException
132     {
133         XPagetal/web/xpages/XPage.html#XPage">XPage page = new XPage( );
134         String strQuery = request.getParameter( PARAMETER_QUERY );
135         String strTagFilter = request.getParameter( PARAMETER_TAG_FILTER );
136 
137         String strEncoding = AppPropertiesService.getProperty( PROPERTY_ENCODE_URI_ENCODING, StandardCharsets.ISO_8859_1.name( ) );
138 
139         if ( StringUtils.equalsIgnoreCase( CONSTANT_HTTP_METHOD_GET, request.getMethod( ) )
140                 && !StringUtils.equalsIgnoreCase( strEncoding, StandardCharsets.UTF_8.name( ) ) )
141         {
142             try
143             {
144                 if ( StringUtils.isNotBlank( strQuery ) )
145                 {
146                     strQuery = new String( strQuery.getBytes( strEncoding ), StandardCharsets.UTF_8 );
147                 }
148 
149                 if ( StringUtils.isNotBlank( strTagFilter ) )
150                 {
151                     strTagFilter = new String( strTagFilter.getBytes( strEncoding ), StandardCharsets.UTF_8 );
152                 }
153             }
154             catch( UnsupportedEncodingException e )
155             {
156                 AppLogService.error( e.getMessage( ), e );
157             }
158         }
159 
160         if ( StringUtils.isNotEmpty( strTagFilter ) )
161         {
162             strQuery = strTagFilter;
163         }
164 
165         boolean bEncodeUri = Boolean.parseBoolean( AppPropertiesService.getProperty( PROPERTY_ENCODE_URI, Boolean.toString( DEFAULT_ENCODE_URI ) ) );
166 
167         String strSearchPageUrl = AppPropertiesService.getProperty( PROPERTY_SEARCH_PAGE_URL );
168         String strError = "";
169         Locale locale = request.getLocale( );
170 
171         // Check XSS characters
172         if ( ( strQuery != null ) && ( SecurityUtil.containsXssCharacters( request, strQuery ) ) )
173         {
174             strError = I18nService.getLocalizedString( MESSAGE_INVALID_SEARCH_TERMS, locale );
175             strQuery = "";
176         }
177 
178         String strDefaultNbItemPerPage = AppPropertiesService.getProperty( PROPERTY_RESULTS_PER_PAGE, DEFAULT_RESULTS_PER_PAGE );
179         String strNbItemPerPage = Optional.ofNullable( request.getParameter( PARAMETER_NB_ITEMS_PER_PAGE ) ).orElse( strDefaultNbItemPerPage );
180         int nNbItemsPerPage = Integer.parseInt( strNbItemPerPage );
181 
182         String strCurrentPageIndex = Optional.ofNullable( request.getParameter( PARAMETER_PAGE_INDEX ) ).orElse( DEFAULT_PAGE_INDEX );
183 
184         List<SearchResult> listResults = _engine.getSearchResults( strQuery, request );
185 
186         // The page should not be added to the cache
187 
188         // Notify results infos to QueryEventListeners
189         notifyQueryListeners( strQuery, listResults.size( ), request );
190 
191         UrlItem/url/UrlItem.html#UrlItem">UrlItem url = new UrlItem( strSearchPageUrl );
192         String strQueryForPaginator = strQuery;
193 
194         if ( bEncodeUri )
195         {
196             strQueryForPaginator = encodeUrl( request, strQuery );
197         }
198 
199         if ( StringUtils.isNotBlank( strTagFilter ) )
200         {
201             strQuery = "";
202         }
203 
204         url.addParameter( PARAMETER_QUERY, strQueryForPaginator );
205         url.addParameter( PARAMETER_NB_ITEMS_PER_PAGE, nNbItemsPerPage );
206 
207         StringBuilder sbUrl = new StringBuilder( );
208         sbUrl = sbUrl.append( url.getUrl( ) );
209 
210         Map<String, Object> model = new HashMap<>( );
211         if ( StringUtils.isNotBlank( request.getParameter( PARAMETER_DEFAULT_OPERATOR ) ) )
212         {
213             sbUrl = sbUrl.append( "&default_operator=" + request.getParameter( PARAMETER_DEFAULT_OPERATOR ) );
214             // Override default_operator value
215             model.put( PARAMETER_DEFAULT_OPERATOR, request.getParameter( PARAMETER_DEFAULT_OPERATOR ) );
216         }
217 
218         Paginator<SearchResult> paginator = new Paginator<>( listResults, nNbItemsPerPage, sbUrl.toString( ), PARAMETER_PAGE_INDEX, strCurrentPageIndex );
219 
220         model.put( MARK_RESULTS_LIST, paginator.getPageItems( ) );
221         model.put( MARK_QUERY, strQuery );
222         model.put( MARK_PAGINATOR, paginator );
223         model.put( MARK_NB_ITEMS_PER_PAGE, strNbItemPerPage );
224         model.put( MARK_ERROR, strError );
225 
226         ISponsoredLinksSearchService sponsoredLinksService = new SponsoredLinksSearchService( );
227 
228         if ( sponsoredLinksService.isAvailable( ) )
229         {
230             model.put( MARK_SPONSOREDLINKS_SET, sponsoredLinksService.getHtmlCode( strQuery, locale ) );
231         }
232 
233         model.put( MARK_LIST_TYPE_AND_LINK, SearchService.getSearchTypesAndLinks( ) );
234         model.putAll( SearchParameterHome.findAll( ) );
235 
236         HtmlTemplate template = AppTemplateService.getTemplate( TEMPLATE_RESULTS, locale, model );
237         page.setPathLabel( I18nService.getLocalizedString( PROPERTY_PATH_LABEL, locale ) );
238         page.setTitle( I18nService.getLocalizedString( PROPERTY_PAGE_TITLE, locale ) );
239         page.setContent( template.getHtml( ) );
240 
241         return page;
242     }
243 
244     /**
245      * Encode an url string
246      * 
247      * @param request
248      *            The HTTP request
249      * @param strSource
250      *            The string to encode
251      * @return The encoded string
252      * @throws SiteMessageException
253      *             If an error occurs
254      */
255     public static String encodeUrl( HttpServletRequest request, String strSource ) throws SiteMessageException
256     {
257         String strSourceUrl = ( strSource != null ) ? strSource : StringUtils.EMPTY;
258 
259         String strEncoded = EncodingService.encodeUrl( strSourceUrl, PROPERTY_ENCODE_URI_ENCODING, StandardCharsets.ISO_8859_1.name( ) );
260 
261         if ( StringUtils.isBlank( strEncoded ) && StringUtils.isNotBlank( strSourceUrl ) )
262         {
263             SiteMessageService.setMessage( request, MESSAGE_ENCODING_ERROR, SiteMessage.TYPE_ERROR );
264         }
265 
266         return strEncoded;
267     }
268 
269     /**
270      * Notify all query Listeners
271      * 
272      * @param strQuery
273      *            The query
274      * @param nResultsCount
275      *            The results count
276      * @param request
277      *            The request
278      */
279     private void notifyQueryListeners( String strQuery, int nResultsCount, HttpServletRequest request )
280     {
281         QueryEventrvice/search/QueryEvent.html#QueryEvent">QueryEvent event = new QueryEvent( );
282         event.setQuery( strQuery );
283         event.setResultsCount( nResultsCount );
284         event.setRequest( request );
285         QueryListenersService.getInstance( ).notifyListeners( event );
286     }
287 }