View Javadoc
1   /*
2    * Copyright (c) 2002-2020, 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.plugins.announce.service.announcesearch;
35  
36  import java.io.IOException;
37  import java.nio.file.Paths;
38  import java.text.DecimalFormat;
39  import java.text.NumberFormat;
40  import java.util.ArrayList;
41  import java.util.Date;
42  import java.util.List;
43  
44  import org.apache.commons.lang.StringUtils;
45  import org.apache.lucene.analysis.Analyzer;
46  import org.apache.lucene.analysis.miscellaneous.LimitTokenCountAnalyzer;
47  import org.apache.lucene.index.DirectoryReader;
48  import org.apache.lucene.index.IndexReader;
49  import org.apache.lucene.index.IndexWriter;
50  import org.apache.lucene.index.IndexWriterConfig;
51  import org.apache.lucene.index.IndexWriterConfig.OpenMode;
52  import org.apache.lucene.index.LogDocMergePolicy;
53  import org.apache.lucene.index.LogMergePolicy;
54  import org.apache.lucene.search.IndexSearcher;
55  import org.apache.lucene.store.Directory;
56  import org.apache.lucene.store.FSDirectory;
57  
58  import fr.paris.lutece.plugins.announce.business.Announce;
59  import fr.paris.lutece.plugins.announce.business.AnnounceSearchFilter;
60  import fr.paris.lutece.plugins.announce.business.AnnounceSort;
61  import fr.paris.lutece.plugins.announce.business.IndexerAction;
62  import fr.paris.lutece.plugins.announce.business.IndexerActionFilter;
63  import fr.paris.lutece.plugins.announce.business.IndexerActionHome;
64  import fr.paris.lutece.plugins.announce.service.AnnouncePlugin;
65  import fr.paris.lutece.portal.service.plugin.Plugin;
66  import fr.paris.lutece.portal.service.plugin.PluginService;
67  import fr.paris.lutece.portal.service.search.SearchResult;
68  import fr.paris.lutece.portal.service.spring.SpringContextService;
69  import fr.paris.lutece.portal.service.util.AppException;
70  import fr.paris.lutece.portal.service.util.AppLogService;
71  import fr.paris.lutece.portal.service.util.AppPathService;
72  import fr.paris.lutece.portal.service.util.AppPropertiesService;
73  
74  /**
75   * AnnounceSearchService
76   */
77  public final class AnnounceSearchService
78  {
79      private static final String BEAN_SEARCH_ENGINE = "announce.announceSearchEngine";
80      private static final String PATH_INDEX = "announce.internalIndexer.lucene.indexPath";
81      private static final String PROPERTY_WRITER_MERGE_FACTOR = "announce.internalIndexer.lucene.writer.mergeFactor";
82      private static final String PROPERTY_WRITER_MAX_FIELD_LENGTH = "announce.internalIndexer.lucene.writer.maxSectorLength";
83      private static final String PROPERTY_ANALYSER_CLASS_NAME = "announce.internalIndexer.lucene.analyser.className";
84      private static final String PROPERTY_INDEXER_PRICE_FORMAT = "announce.indexer.priceFormat";
85  
86      // Constants
87      private static final String CONSTANT_BLANK_SPACE = " ";
88      private static final String CONSTANT_COMA = ",";
89      private static final String CONSTANT_POINT = ".";
90      private static final String CONSTANT_EURO = "€";
91  
92      // Default values
93      private static final int DEFAULT_WRITER_MERGE_FACTOR = 20;
94      private static final int DEFAULT_WRITER_MAX_FIELD_LENGTH = 1000000;
95  
96      // Constants corresponding to the variables defined in the lutece.properties file
97      private static volatile AnnounceSearchService _singleton;
98      private static String _strPriceFormat;
99      private volatile String _strIndex;
100     private Analyzer _analyzer;
101     private IAnnounceSearchIndexer _indexer;
102     private int _nWriterMergeFactor;
103     private int _nWriterMaxSectorLength;
104 
105     /**
106      * Creates a new instance of DirectorySearchService
107      */
108     private AnnounceSearchService( )
109     {
110         // Read configuration properties
111         String strIndex = getIndex( );
112 
113         if ( StringUtils.isEmpty( strIndex ) )
114         {
115             throw new AppException( "Lucene index path not found in announce.properties", null );
116         }
117 
118         _nWriterMergeFactor = AppPropertiesService.getPropertyInt( PROPERTY_WRITER_MERGE_FACTOR, DEFAULT_WRITER_MERGE_FACTOR );
119         _nWriterMaxSectorLength = AppPropertiesService.getPropertyInt( PROPERTY_WRITER_MAX_FIELD_LENGTH, DEFAULT_WRITER_MAX_FIELD_LENGTH );
120 
121         String strAnalyserClassName = AppPropertiesService.getProperty( PROPERTY_ANALYSER_CLASS_NAME );
122 
123         if ( ( strAnalyserClassName == null ) || ( strAnalyserClassName.equals( "" ) ) )
124         {
125             throw new AppException( "Analyser class name not found in announce.properties", null );
126         }
127 
128         _indexer = SpringContextService.getBean( "announce.announceIndexer" );
129 
130         try
131         {
132             _analyzer = (Analyzer) Class.forName( strAnalyserClassName ).newInstance( );
133         }
134         catch( Exception e )
135         {
136             throw new AppException( "Failed to load Lucene Analyzer class", e );
137         }
138     }
139 
140     /**
141      * Get the HelpdeskSearchService instance
142      * 
143      * @return The {@link AnnounceSearchService}
144      */
145     public static AnnounceSearchService getInstance( )
146     {
147         if ( _singleton == null )
148         {
149             _singleton = new AnnounceSearchService( );
150         }
151 
152         return _singleton;
153     }
154 
155     /**
156      * Return search results
157      * 
158      * @param filter
159      *            The search filter
160      * @param nPageNumber
161      *            The current page
162      * @param nItemsPerPage
163      *            The number of items per page to get
164      * @param listIdAnnounces
165      *            Results as a collection of id of announces
166      * @return The total number of items found
167      */
168     public int getSearchResults( AnnounceSearchFilter filter, int nPageNumber, int nItemsPerPage, List<Integer> listIdAnnounces )
169     {
170         int nNbItems = 0;
171 
172         try
173         {
174             IAnnounceSearchEngine engine = SpringContextService.getBean( BEAN_SEARCH_ENGINE );
175             List<SearchResult> listResults = new ArrayList<>( );
176             nNbItems = engine.getSearchResults( filter, PluginService.getPlugin( AnnouncePlugin.PLUGIN_NAME ), listResults, nPageNumber, nItemsPerPage );
177 
178             for ( SearchResult searchResult : listResults )
179             {
180                 if ( searchResult.getId( ) != null )
181                 {
182                     listIdAnnounces.add( Integer.parseInt( searchResult.getId( ) ) );
183                 }
184             }
185         }
186         catch( Exception e )
187         {
188             AppLogService.error( e.getMessage( ), e );
189             // If an error occurred clean result list
190             listIdAnnounces.clear( );
191         }
192 
193         return nNbItems;
194     }
195 
196     public int getSearchResultsBis( AnnounceSearchFilter filter, int nPageNumber, int nItemsPerPage, List<Announce> listAnnouncesResults, AnnounceSort anSort )
197     {
198         int nNbItems = 0;
199 
200         try
201         {
202             IAnnounceSearchEngine engine = SpringContextService.getBean( BEAN_SEARCH_ENGINE );
203             nNbItems = engine.getSearchResultsBis( filter, PluginService.getPlugin( AnnouncePlugin.PLUGIN_NAME ), listAnnouncesResults, nPageNumber,
204                     nItemsPerPage, anSort );
205 
206         }
207         catch( Exception e )
208         {
209             AppLogService.error( e.getMessage( ), e );
210             // If an error occurred clean result list
211             listAnnouncesResults.clear( );
212         }
213 
214         return nNbItems;
215     }
216 
217     /**
218      * return searcher
219      * 
220      * @return searcher
221      */
222     public IndexSearcher getSearcher( )
223     {
224         IndexSearcher searcher = null;
225 
226         try
227         {
228             IndexReader ir = DirectoryReader.open( FSDirectory.open( Paths.get( getIndex( ) ) ) );
229             searcher = new IndexSearcher( ir );
230         }
231         catch( IOException e )
232         {
233             AppLogService.error( e.getMessage( ), e );
234         }
235 
236         return searcher;
237     }
238 
239     /**
240      * Process indexing
241      * 
242      * @param bCreate
243      *            true for start full indexing false for begin incremental indexing
244      * @return the log
245      */
246     public String processIndexing( boolean bCreate )
247     {
248         StringBuffer sbLogs = new StringBuffer( );
249         IndexWriter writer = null;
250         boolean bCreateIndex = bCreate;
251 
252         try
253         {
254             sbLogs.append( "\r\nIndexing all contents ...\r\n" );
255 
256             Directory dir = FSDirectory.open( Paths.get( getIndex( ) ) );
257 
258             // Nouveau
259             if ( !DirectoryReader.indexExists( dir ) )
260             { // init index
261                 bCreateIndex = true;
262             }
263 
264             boolean bIsLocked = false;
265 
266             if ( IndexWriter.isLocked( dir ) )
267             {
268                 sbLogs.append( "AnnounceSearchService, the index is locked. Aborting." );
269             }
270 
271             if ( !bIsLocked )
272             {
273                 Date start = new Date( );
274 
275                 IndexWriterConfig conf = new IndexWriterConfig( new LimitTokenCountAnalyzer( _analyzer, _nWriterMaxSectorLength ) );
276                 LogMergePolicy mergePolicy = new LogDocMergePolicy( );
277                 mergePolicy.setMergeFactor( _nWriterMergeFactor );
278 
279                 conf.setMergePolicy( mergePolicy );
280 
281                 if ( bCreateIndex )
282                 {
283                     conf.setOpenMode( OpenMode.CREATE );
284                 }
285                 else
286                 {
287                     conf.setOpenMode( OpenMode.APPEND );
288                 }
289 
290                 writer = new IndexWriter( dir, conf );
291 
292                 start = new Date( );
293 
294                 sbLogs.append( "\r\n<strong>Indexer : " );
295                 sbLogs.append( _indexer.getName( ) );
296                 sbLogs.append( " - " );
297                 sbLogs.append( _indexer.getDescription( ) );
298                 sbLogs.append( "</strong>\r\n" );
299                 _indexer.processIndexing( writer, bCreateIndex, sbLogs );
300 
301                 Date end = new Date( );
302 
303                 sbLogs.append( "Duration of the treatment : " );
304                 sbLogs.append( end.getTime( ) - start.getTime( ) );
305                 sbLogs.append( " milliseconds\r\n" );
306             }
307         }
308         catch( Exception e )
309         {
310             sbLogs.append( " caught a " );
311             sbLogs.append( e.getClass( ) );
312             sbLogs.append( "\n with message: " );
313             sbLogs.append( e.getMessage( ) );
314             sbLogs.append( "\r\n" );
315             AppLogService.error( "Indexing error : " + e.getMessage( ), e );
316         }
317         finally
318         {
319             try
320             {
321                 if ( writer != null )
322                 {
323                     writer.close( );
324                 }
325             }
326             catch( IOException e )
327             {
328                 AppLogService.error( e.getMessage( ), e );
329             }
330         }
331 
332         return sbLogs.toString( );
333     }
334 
335     /**
336      * Add Indexer Action to perform on a record
337      * 
338      * @param nIdAnnounce
339      *            announce id
340      * @param nIdTask
341      *            the key of the action to do
342      * @param plugin
343      *            the plugin
344      */
345     public void addIndexerAction( int nIdAnnounce, int nIdTask, Plugin plugin )
346     {
347         IndexerActionbusiness/IndexerAction.html#IndexerAction">IndexerAction indexerAction = new IndexerAction( );
348         indexerAction.setIdAnnounce( nIdAnnounce );
349         indexerAction.setIdTask( nIdTask );
350         IndexerActionHome.create( indexerAction );
351     }
352 
353     /**
354      * Remove a Indexer Action
355      * 
356      * @param nIdAction
357      *            the key of the action to remove
358      * @param plugin
359      *            the plugin
360      */
361     public void removeIndexerAction( int nIdAction, Plugin plugin )
362     {
363         IndexerActionHome.remove( nIdAction );
364     }
365 
366     /**
367      * return a list of IndexerAction by task key
368      * 
369      * @param nIdTask
370      *            the task key
371      * @param plugin
372      *            the plugin
373      * @return a list of IndexerAction
374      */
375     public List<IndexerAction> getAllIndexerActionByTask( int nIdTask, Plugin plugin )
376     {
377         IndexerActionFilter/business/IndexerActionFilter.html#IndexerActionFilter">IndexerActionFilter filter = new IndexerActionFilter( );
378         filter.setIdTask( nIdTask );
379 
380         return IndexerActionHome.getList( filter );
381     }
382 
383     /**
384      * Get the path to the index of the search service
385      * 
386      * @return The path to the index of the search service
387      */
388     private String getIndex( )
389     {
390         if ( _strIndex == null )
391         {
392             _strIndex = AppPathService.getPath( PATH_INDEX );
393         }
394 
395         return _strIndex;
396     }
397 
398     /**
399      * Get the analyzed of this search service
400      * 
401      * @return The analyzer of this search service
402      */
403     public Analyzer getAnalyzer( )
404     {
405         return _analyzer;
406     }
407 
408     /**
409      * Format a price for the indexer
410      * 
411      * @param dPrice
412      *            The price to format
413      * @return The formated price
414      */
415     public static String formatPriceForIndexer( double dPrice )
416     {
417         NumberFormat formatter = new DecimalFormat( getPriceFormat( ) );
418 
419         return formatter.format( dPrice );
420     }
421 
422     /**
423      * Format a numerous string
424      * 
425      * @param strPrice
426      *            The price
427      * @return The formated price
428      */
429     public static String getFormatedPriceString( String strPrice )
430     {
431         return strPrice.replace( CONSTANT_BLANK_SPACE, StringUtils.EMPTY ).replace( CONSTANT_COMA, CONSTANT_POINT )
432                 .replace( CONSTANT_EURO, StringUtils.EMPTY ).trim( );
433     }
434 
435     /**
436      * Format a price for the indexer
437      * 
438      * @param nPrice
439      *            The price to format
440      * @return The formated price
441      */
442     public static String formatPriceForIndexer( int nPrice )
443     {
444         NumberFormat formatter = new DecimalFormat( getPriceFormat( ) );
445 
446         return formatter.format( nPrice );
447     }
448 
449     /**
450      * Get the price format to use
451      * 
452      * @return the price format to use
453      */
454     private static String getPriceFormat( )
455     {
456         if ( _strPriceFormat == null )
457         {
458             _strPriceFormat = AppPropertiesService.getProperty( PROPERTY_INDEXER_PRICE_FORMAT, "#0000000000.00" );
459         }
460 
461         return _strPriceFormat;
462     }
463 }