/*
* Copyright 2002-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springmodules.lucene.index.object.database;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.sql.DataSource;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.MappingSqlQuery;
import org.springmodules.lucene.index.LuceneIndexAccessException;
import org.springmodules.lucene.index.LuceneIndexingException;
import org.springmodules.lucene.index.document.handler.DocumentHandler;
import org.springmodules.lucene.index.document.handler.database.SqlDocumentHandler;
import org.springmodules.lucene.index.document.handler.database.SqlRequest;
import org.springmodules.lucene.index.factory.IndexFactory;
import org.springmodules.lucene.index.factory.IndexWriterFactoryUtils;
import org.springmodules.lucene.index.factory.LuceneIndexWriter;
import org.springmodules.lucene.index.object.AbstractIndexer;
/**
* <b>This is the central class in the lucene database indexing package.</b>
* It simplifies the use of lucene to index a database specifying the requests
* to execute and how to index the corresponding rows.
* It helps to avoid common errors and to manage these resource in a flexible
* manner.
* It executes core Lucene workflow, leaving application code to focus on
* the way to create Lucene documents from a row for a request.
*
* <p>This class is based on the IndexFactory abstraction which is a
* factory to create IndexWriter for the configured
* Directory. For the execution and the indexation of the corresponding
* data, the indexer uses the same IndexWriter. It calls the IndexWriterFactoryUtils
* class to eventually release it. So the indexer doesn't need to always
* hold resources during the indexation of every requests and
* this avoids some locking problems on the index. You can too apply
* different strategies for managing index resources.
*
* <p>Can be used within a service implementation via direct instantiation
* with a IndexFactory reference, or get prepared in an application context
* and given to services as bean reference. Note: The IndexFactory should
* always be configured as a bean in the application context, in the first case
* given to the service directly, in the second case to the prepared template.
*
* @author Thierry Templier
* @see org.springmodules.lucene.index.object.AbstractIndexer
* @see org.springmodules.lucene.index.factory.IndexFactory
* @see org.springmodules.lucene.index.support.database.SqlDocumentHandler
* @see org.springmodules.lucene.index.object.database.DatabaseIndexingListener
* @see org.springmodules.lucene.index.factory.IndexWriterFactoryUtils#getIndexWriter(IndexFactory)
* @see org.springmodules.lucene.index.factory.IndexWriterFactoryUtils#releaseIndexWriter(IndexFactory, IndexWriter)
*/
public class DefaultDatabaseIndexer extends AbstractIndexer implements DatabaseIndexer {
private Map requestDocumentHandlers;
private List listeners;
/**
* Construct a new DatabaseIndexer, given an IndexFactory to obtain IndexWriter.
*
* @param indexFactory IndexFactory to obtain IndexWriter
*/
public DefaultDatabaseIndexer(IndexFactory indexFactory) {
setIndexFactory(indexFactory);
requestDocumentHandlers=new HashMap();
listeners=new ArrayList();
registerDefautHandlers();
}
/**
* This method specifies the default handlers to automatically
* register when the indexer is instantiated.
*
* <p>This method is empty but you can overwrite it to specify
* your default handlers.
*/
protected void registerDefautHandlers() {
}
/**
* This method is used to register a request to execute and an handler
* to specify how to index the rows corresponding to the request.
*
* <p>The request is specify with an instance of the SqlRequest class
* which contains the sql requests, the parameter types and values.
* The implementation of the SqlDocumentHandler class defines a callback
* method which will be called for every row to index its data.
*
* @param sqlRequest the request to execute
* @param handler the handler to index the rows
*/
public void registerDocumentHandler(SqlRequest sqlRequest, SqlDocumentHandler handler) {
if( sqlRequest!=null && handler!=null ) {
requestDocumentHandlers.put(sqlRequest, handler);
}
}
/**
* This method is used to unregister a request.
*
* @param sqlRequest the request to execute
*/
public void unregisterDocumentHandler(SqlRequest sqlRequest) {
if( sqlRequest!=null ) {
requestDocumentHandlers.remove(sqlRequest);
}
}
/**
* This method is used to get the SqlDocumentHandler implementation
* corresponding to SqlRequest passed as parameter.
*
* @param sqlRequest the request to execute
* @return the handler to index the rows
*/
public SqlDocumentHandler getDocumentHandler(SqlRequest sqlRequest) {
if( sqlRequest!=null ) {
return (SqlDocumentHandler)requestDocumentHandlers.get(sqlRequest);
} else {
return null;
}
}
/**
* This method returns all the registred requests and their
* corresponding handlers.
*
* @return a map containing all the registred requests and their
* corresponding handlers
*/
public Map getDocumentHandlers() {
return requestDocumentHandlers;
}
/**
* This method is used to add a listener to be notified during the
* indexing execution.
*
* @param listener the listener to add
*/
public void addListener(DatabaseIndexingListener listener) {
if( listener!=null ) {
listeners.add(listener);
}
}
/**
* This method is used to remove a specified listener.
*
* @param listener the listener to remove
*/
public void removeListener(DatabaseIndexingListener listener) {
if( listener!=null ) {
listeners.remove(listener);
}
}
/**
* This method is used to get the list of listeners to notify
* during the indexing execution.
*
* @return the list of listeners to notify
*/
public List getListeners() {
return listeners;
}
/**
* This method is used to fire the "on before request" event to
* every listeners.
*
* <p>This event will be fired before the request execution and
* the indexing of rows, even if there is an sql or indexing
* error.
*
* @param request the request which will be executed
*/
protected void fireListenersOnBeforeRequest(SqlRequest request) {
for(Iterator i=listeners.iterator();i.hasNext();) {
DatabaseIndexingListener listener=(DatabaseIndexingListener)i.next();
listener.beforeIndexingRequest(request);
}
}
/**
* This method is used to fire the "on after request" event to
* every listeners.
*
* <p>This event will be fired after the request execution and
* the indexing of rows. It will not happen if there is an sql
* or indexing error.
*
* @param request the request which has been executed
*/
protected void fireListenersOnAfterRequest(SqlRequest request) {
for(Iterator i=listeners.iterator();i.hasNext();) {
DatabaseIndexingListener listener=(DatabaseIndexingListener)i.next();
listener.afterIndexingRequest(request);
}
}
/**
* This method is used to fire the "on error request" event to
* every listeners.
*
* <p>This event will be fired if there is an sql or indexing error.
*
* @param request the request which has been executed
*/
protected void fireListenersOnErrorRequest(SqlRequest request,Exception ex) {
for(Iterator i=listeners.iterator();i.hasNext();) {
DatabaseIndexingListener listener=(DatabaseIndexingListener)i.next();
listener.onErrorIndexingRequest(request,ex);
}
}
/**
* This method executes the sql request on a specified datasource with or
* with sql parameters. It is based on the Spring JDBC framework and uses
* a sub class of MappingSqlQuery.
*
* <p>It is based on the IndexingMappingQuery class which delegates its mapRow
* method to the getDocument of a SqlDocumentHandler implementation. The
*
* @param dataSource the datasource to use
* @param request the request to execute
* @param handler the handler to use to index the rows
* @return the list of Lucene documents add to the index
* @see IndexingMappingQuery
*/
private List indexResultSql(DataSource dataSource,SqlRequest request,SqlDocumentHandler handler) {
IndexingMappingQuery query=new IndexingMappingQuery(dataSource,request,handler);
if( request.getParams()!=null ) {
return query.execute(request.getParams());
} else {
return query.execute();
}
}
/**
* This method adds a list of Lucene documents to an index using an
* corresponding IndexWriter instance.
* @param writer the IndexWriter instance to use
* @param documents the list of documents to add
*/
private void addDocumentsInIndex(LuceneIndexWriter writer, List documents) throws IOException{
for(Iterator i=documents.iterator(); i.hasNext();) {
Document document = (Document)i.next();
if( document!=null ) {
writer.addDocument(document);
}
}
}
/**
* This method executes the request and adds the result of indexing
* of rows in the index. It gets an IndexWriter instance from the
* configured IndexFactory and releases it if necessary. It manages
* too both exceptions thrown during the request execution and the
* indexing of rows.
*
* <p>Before the return of the method, it optimizes too the index
* if the value of the optimizeIndex parameter is true.
*
* @param dataSource the datasource to use
* @param request the request to execute
* @param handler the handler to use to index the rows
* @param optimizeIndex if the index must be optimized after
* the request indexing
* @see IndexWriterFactoryUtils#getIndexWriter(IndexFactory)
* @see IndexWriterFactoryUtils#releaseIndexWriter(IndexFactory, IndexWriter)
* @see #fireListenersOnBeforeRequest(SqlRequest)
* @see #fireListenersOnAfterRequest(SqlRequest)
* @see #fireListenersOnErrorRequest(SqlRequest, Exception)
*/
private void doHandleRequest(LuceneIndexWriter writer, DataSource dataSource,
SqlRequest request, SqlDocumentHandler handler) {
try {
fireListenersOnBeforeRequest(request);
List documents = indexResultSql(dataSource, request, handler);
if( documents!=null ) {
addDocumentsInIndex(writer, documents);
}
fireListenersOnAfterRequest(request);
} catch(DataAccessException ex) {
logger.error("Error during indexing the request", ex);
fireListenersOnErrorRequest(request, ex);
} catch(IOException ex) {
logger.error("Error during indexing the request", ex);
fireListenersOnErrorRequest(request, ex);
}
}
/**
* This method is the entry point to index a database using the specified
* datasource. It uses the registred requests and their corresponding handlers.
*
* <p>In this case, the index will not be optimized.
*
* @param dataSource the datasource to use
* @see #index(DataSource, boolean)
*/
public void index(DataSource dataSource) {
index(dataSource, false);
}
/**
* This method is the entry point to index a database using the specified
* datasource. It uses the registred requests and their corresponding handlers.
*
* <p>In this case, the index will be optimized at the end
* if the value of the optimizeIndex parameter is true.
*
* <p>If there is an error during executing a request or indexing rows,
* the other requests will be executed. However the error will notify to
* specified listeners.
*
* @param dataSource the datasource to use
* @param optimizeIndex if the index must be optimized after
* the request indexing
* @see #doHandleRequest(DataSource, SqlRequest, SqlDocumentHandler, boolean)
*/
public void index(DataSource dataSource, boolean optimizeIndex) {
LuceneIndexWriter writer = IndexWriterFactoryUtils.getIndexWriter(getIndexFactory());
try {
Set requests = requestDocumentHandlers.keySet();
for(Iterator i=requests.iterator(); i.hasNext();) {
SqlRequest request = (SqlRequest)i.next();
SqlDocumentHandler handler = (SqlDocumentHandler)requestDocumentHandlers.get(request);
doHandleRequest(writer, dataSource, request, handler);
}
//Optimize the index
if( optimizeIndex ) {
writer.optimize();
}
} catch(Exception ex) {
ex.printStackTrace();
logger.error("Error during indexing the datasource", ex);
throw new LuceneIndexAccessException("Error during indexing the datasource", ex);
} catch(Throwable t) {
t.printStackTrace();
} finally {
IndexWriterFactoryUtils.releaseIndexWriter(getIndexFactory(), writer);
}
}
/**
* This is the sub class MappingSqlQuery used to delegate the mapRow callback
* to the getDocument of the specified SqlDocumentHandler for each result row
* of the request.
*/
private static class IndexingMappingQuery extends MappingSqlQuery {
private SqlRequest request;
private DocumentHandler handler;
public IndexingMappingQuery(DataSource ds, SqlRequest request, DocumentHandler handler) {
super(ds, request.getSql());
this.request = request;
this.handler = handler;
int[] types = request.getTypes();
if( types!=null ) {
for(int cpt=0; cpt<types.length; cpt++) {
super.declareParameter(new SqlParameter(types[cpt]));
}
}
compile();
}
public Object mapRow(ResultSet rs, int rowNumber) throws SQLException {
try {
return handler.getDocument(request.getDescription(), rs);
} catch (SQLException ex) {
throw ex;
} catch (Exception ex) {
throw new LuceneIndexingException("Error during the indexing of the ResultSet.", ex);
}
}
}
}