AnnounceSearchService.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.announce.service.announcesearch;
import java.io.IOException;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.miscellaneous.LimitTokenCountAnalyzer;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.LogDocMergePolicy;
import org.apache.lucene.index.LogMergePolicy;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import fr.paris.lutece.plugins.announce.business.Announce;
import fr.paris.lutece.plugins.announce.business.AnnounceSearchFilter;
import fr.paris.lutece.plugins.announce.business.AnnounceSort;
import fr.paris.lutece.plugins.announce.business.IndexerAction;
import fr.paris.lutece.plugins.announce.business.IndexerActionFilter;
import fr.paris.lutece.plugins.announce.business.IndexerActionHome;
import fr.paris.lutece.plugins.announce.service.AnnouncePlugin;
import fr.paris.lutece.portal.service.plugin.Plugin;
import fr.paris.lutece.portal.service.plugin.PluginService;
import fr.paris.lutece.portal.service.search.SearchResult;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppException;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPathService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
/**
* AnnounceSearchService
*/
public final class AnnounceSearchService
{
private static final String BEAN_SEARCH_ENGINE = "announce.announceSearchEngine";
private static final String PATH_INDEX = "announce.internalIndexer.lucene.indexPath";
private static final String PROPERTY_WRITER_MERGE_FACTOR = "announce.internalIndexer.lucene.writer.mergeFactor";
private static final String PROPERTY_WRITER_MAX_FIELD_LENGTH = "announce.internalIndexer.lucene.writer.maxSectorLength";
private static final String PROPERTY_ANALYSER_CLASS_NAME = "announce.internalIndexer.lucene.analyser.className";
private static final String PROPERTY_INDEXER_PRICE_FORMAT = "announce.indexer.priceFormat";
// Constants
private static final String CONSTANT_BLANK_SPACE = " ";
private static final String CONSTANT_COMA = ",";
private static final String CONSTANT_POINT = ".";
private static final String CONSTANT_EURO = "€";
// Default values
private static final int DEFAULT_WRITER_MERGE_FACTOR = 20;
private static final int DEFAULT_WRITER_MAX_FIELD_LENGTH = 1000000;
// Constants corresponding to the variables defined in the lutece.properties file
private static volatile AnnounceSearchService _singleton;
private static String _strPriceFormat;
private volatile String _strIndex;
private Analyzer _analyzer;
private IAnnounceSearchIndexer _indexer;
private int _nWriterMergeFactor;
private int _nWriterMaxSectorLength;
private static volatile IndexWriter _indexWriterInstance;
/**
* Creates a new instance of DirectorySearchService
*/
private AnnounceSearchService( )
{
// Read configuration properties
String strIndex = getIndex( );
if ( StringUtils.isEmpty( strIndex ) )
{
throw new AppException( "Lucene index path not found in announce.properties", null );
}
_nWriterMergeFactor = AppPropertiesService.getPropertyInt( PROPERTY_WRITER_MERGE_FACTOR, DEFAULT_WRITER_MERGE_FACTOR );
_nWriterMaxSectorLength = AppPropertiesService.getPropertyInt( PROPERTY_WRITER_MAX_FIELD_LENGTH, DEFAULT_WRITER_MAX_FIELD_LENGTH );
String strAnalyserClassName = AppPropertiesService.getProperty( PROPERTY_ANALYSER_CLASS_NAME );
if ( ( strAnalyserClassName == null ) || ( strAnalyserClassName.equals( "" ) ) )
{
throw new AppException( "Analyser class name not found in announce.properties", null );
}
_indexer = SpringContextService.getBean( "announce.announceIndexer" );
try
{
_analyzer = ( Analyzer ) Class.forName( strAnalyserClassName ).newInstance( );
}
catch( Exception e )
{
throw new AppException( "Failed to load Lucene Analyzer class", e );
}
}
/**
* Get the HelpdeskSearchService instance
*
* @return The {@link AnnounceSearchService}
*/
public static AnnounceSearchService getInstance( )
{
if ( _singleton == null )
{
_singleton = new AnnounceSearchService( );
}
return _singleton;
}
/**
* Return search results
*
* @param filter
* The search filter
* @param nPageNumber
* The current page
* @param nItemsPerPage
* The number of items per page to get
* @param listIdAnnounces
* Results as a collection of id of announces
* @return The total number of items found
*/
public int getSearchResults( AnnounceSearchFilter filter, int nPageNumber, int nItemsPerPage, List<Integer> listIdAnnounces )
{
int nNbItems = 0;
try
{
IAnnounceSearchEngine engine = SpringContextService.getBean( BEAN_SEARCH_ENGINE );
List<SearchResult> listResults = new ArrayList<>( );
nNbItems = engine.getSearchResults( filter, PluginService.getPlugin( AnnouncePlugin.PLUGIN_NAME ), listResults, nPageNumber, nItemsPerPage );
for ( SearchResult searchResult : listResults )
{
if ( searchResult.getId( ) != null )
{
listIdAnnounces.add( Integer.parseInt( searchResult.getId( ) ) );
}
}
}
catch( Exception e )
{
AppLogService.error( e.getMessage( ), e );
// If an error occurred clean result list
listIdAnnounces.clear( );
}
return nNbItems;
}
public int getSearchResultsBis( AnnounceSearchFilter filter, int nPageNumber, int nItemsPerPage, List<Announce> listAnnouncesResults, AnnounceSort anSort )
{
int nNbItems = 0;
try
{
IAnnounceSearchEngine engine = SpringContextService.getBean( BEAN_SEARCH_ENGINE );
nNbItems = engine.getSearchResultsBis( filter, PluginService.getPlugin( AnnouncePlugin.PLUGIN_NAME ), listAnnouncesResults, nPageNumber,
nItemsPerPage, anSort );
}
catch( Exception e )
{
AppLogService.error( e.getMessage( ), e );
// If an error occurred clean result list
listAnnouncesResults.clear( );
}
return nNbItems;
}
/**
* return searcher
*
* @return searcher
*/
public IndexSearcher getSearcher( )
{
IndexSearcher searcher = null;
try
{
IndexReader ir = DirectoryReader.open( FSDirectory.open( Paths.get( getIndex( ) ) ) );
searcher = new IndexSearcher( ir );
}
catch( IOException e )
{
AppLogService.error( e.getMessage( ), e );
}
return searcher;
}
/**
* Process indexing
*
* @param bCreate
* true for start full indexing false for begin incremental indexing
* @return the log
*/
public String processIndexing( boolean bCreate )
{
StringBuffer sbLogs = new StringBuffer( );
IndexWriter writer = null;
boolean bCreateIndex = bCreate;
try
{
sbLogs.append( "\r\nIndexing all contents ...\r\n" );
Directory dir = FSDirectory.open( Paths.get( getIndex( ) ) );
if ( !DirectoryReader.indexExists( dir ) )
{
bCreateIndex = true;
}
writer = getIndexWriterInstance( bCreateIndex );
Date start = new Date( );
sbLogs.append( "\r\n<strong>Indexer : " );
sbLogs.append( _indexer.getName( ) );
sbLogs.append( " - " );
sbLogs.append( _indexer.getDescription( ) );
sbLogs.append( "</strong>\r\n" );
_indexer.processIndexing( writer, bCreateIndex, sbLogs );
Date end = new Date( );
sbLogs.append( "Duration of the treatment : " );
sbLogs.append( end.getTime( ) - start.getTime( ) );
sbLogs.append( " milliseconds\r\n" );
}
catch( Exception e )
{
sbLogs.append( " caught a " );
sbLogs.append( e.getClass( ) );
sbLogs.append( "\n with message: " );
sbLogs.append( e.getMessage( ) );
sbLogs.append( "\r\n" );
AppLogService.error( "Indexing error : " + e.getMessage( ), e );
}
finally
{
try
{
closeIndexWriterInstance( );
}
catch( IOException e )
{
AppLogService.error( e.getMessage( ), e );
}
}
return sbLogs.toString( );
}
/**
* Add Indexer Action to perform on a record
*
* @param nIdAnnounce
* announce id
* @param nIdTask
* the key of the action to do
* @param plugin
* the plugin
*/
public void addIndexerAction( int nIdAnnounce, int nIdTask, Plugin plugin )
{
IndexerAction indexerAction = new IndexerAction( );
indexerAction.setIdAnnounce( nIdAnnounce );
indexerAction.setIdTask( nIdTask );
IndexerActionHome.create( indexerAction );
}
/**
* Remove a Indexer Action
*
* @param nIdAction
* the key of the action to remove
* @param plugin
* the plugin
*/
public void removeIndexerAction( int nIdAction, Plugin plugin )
{
IndexerActionHome.remove( nIdAction );
}
/**
* return a list of IndexerAction by task key
*
* @param nIdTask
* the task key
* @param plugin
* the plugin
* @return a list of IndexerAction
*/
public List<IndexerAction> getAllIndexerActionByTask( int nIdTask, Plugin plugin )
{
IndexerActionFilter filter = new IndexerActionFilter( );
filter.setIdTask( nIdTask );
return IndexerActionHome.getList( filter );
}
/**
* Get the path to the index of the search service
*
* @return The path to the index of the search service
*/
private String getIndex( )
{
if ( _strIndex == null )
{
_strIndex = AppPathService.getPath( PATH_INDEX );
}
return _strIndex;
}
/**
* Get the analyzed of this search service
*
* @return The analyzer of this search service
*/
public Analyzer getAnalyzer( )
{
return _analyzer;
}
/**
* Format a price for the indexer
*
* @param dPrice
* The price to format
* @return The formated price
*/
public static String formatPriceForIndexer( double dPrice )
{
NumberFormat formatter = new DecimalFormat( getPriceFormat( ) );
return formatter.format( dPrice );
}
/**
* Format a numerous string
*
* @param strPrice
* The price
* @return The formated price
*/
public static String getFormatedPriceString( String strPrice )
{
return strPrice.replace( CONSTANT_BLANK_SPACE, StringUtils.EMPTY ).replace( CONSTANT_COMA, CONSTANT_POINT ).replace( CONSTANT_EURO, StringUtils.EMPTY )
.trim( );
}
/**
* Format a price for the indexer
*
* @param nPrice
* The price to format
* @return The formated price
*/
public static String formatPriceForIndexer( int nPrice )
{
NumberFormat formatter = new DecimalFormat( getPriceFormat( ) );
return formatter.format( nPrice );
}
/**
* Get the price format to use
*
* @return the price format to use
*/
private static String getPriceFormat( )
{
if ( _strPriceFormat == null )
{
_strPriceFormat = AppPropertiesService.getProperty( PROPERTY_INDEXER_PRICE_FORMAT, "#0000000000.00" );
}
return _strPriceFormat;
}
private synchronized IndexWriter getIndexWriterInstance( boolean bCreateIndex ) throws IOException
{
if ( _indexWriterInstance == null )
{
Directory dir = FSDirectory.open(Paths.get( getIndex( ) ) );
IndexWriterConfig conf = new IndexWriterConfig( new LimitTokenCountAnalyzer( _analyzer, _nWriterMaxSectorLength ) );
LogMergePolicy mergePolicy = new LogDocMergePolicy( );
mergePolicy.setMergeFactor( _nWriterMergeFactor );
conf.setMergePolicy(mergePolicy);
if (bCreateIndex)
{
conf.setOpenMode( OpenMode.CREATE );
}
else
{
conf.setOpenMode( OpenMode.APPEND );
}
_indexWriterInstance = new IndexWriter( dir, conf );
}
return _indexWriterInstance;
}
private synchronized void closeIndexWriterInstance( ) throws IOException
{
if ( _indexWriterInstance != null )
{
_indexWriterInstance.close( );
_indexWriterInstance = null;
}
}
}