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