package com.pugh.sockso.music.indexing;
import com.pugh.sockso.Utils;
import com.pugh.sockso.db.Database;
import org.apache.log4j.Logger;
import java.io.File;
import java.io.FileFilter;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Scans and indexes files in the collection.
*
* @TODO - need to be able to handle requests to scan when we're already
* scanning the collection.
*
* @TODO - find way to index modified time of files on first scan
*
*/
public abstract class BaseIndexer implements Indexer {
public static Logger log = Logger.getLogger( BaseIndexer.class );
private final List<IndexListener> indexListeners;
private final Database db;
private final IndexCache cache;
private AtomicBoolean isIndexing;
/**
* Constructor
*
* @param db
*
*/
public BaseIndexer( final Database db ) {
this( db, new IndexCache() );
}
/**
* Constructor, allows injecting the IndexCache to use
*
* @param db
* @param cache
*
*/
public BaseIndexer( final Database db, final IndexCache cache ) {
this.db = db;
this.cache = cache;
indexListeners = new ArrayList<IndexListener>();
isIndexing = new AtomicBoolean(false);
}
/**
* Returns a FileFilter for the specific type of files that are to be
* indexed - BUT should also return directories assuming these are to
* be scanned recursively
*
* @return
*
*/
protected abstract FileFilter getFileFilter();
/**
* Returns the sql to use to extract the file information
*
* @return
*
*/
protected abstract String getFilesSql();
/**
* Returns the sql to use to extract the root directories to scan
*
* @return
*
*/
protected abstract String getDirectoriesSql();
/**
* scans for changes on disk (both new and updated files). IndexEvent
* events are then fired to listeners.
*
*/
@Override
public void scan() {
scan( ScanFilter.MODIFICATION_DATE, ScanScope.ALL_FILES );
}
public void scan( final ScanFilter filter, final ScanScope scope ) {
if ( isIndexing.compareAndSet( false, true ) ) {
final long start = System.currentTimeMillis();
log.debug( "scan() starting..." );
checkIntegrity( filter );
log.debug( "integrity check done: " + (System.currentTimeMillis() - start) );
if ( !ScanScope.EXISTING_FILES.equals(scope) ) {
checkForNewFiles();
}
log.debug( "scan() finished: " + (System.currentTimeMillis() - start) );
fireIndexEvent(
new IndexEvent( IndexEvent.Type.COMPLETE, -1, new File("") )
);
isIndexing.set( false );
}
}
/**
* Checks for new files to add to the index. This assumes that the integrity
* check has already been run so the cache is up to date with all the files
* we currently have in the collection.
*
* New files are signaled with a IndexEvent.UNKNOWN event.
*
*/
protected void checkForNewFiles() {
ResultSet rs = null;
try {
updateCache();
rs = getDirectories();
while ( rs.next() ) {
scan( rs.getInt("id"), new File(rs.getString("path")) );
}
}
catch ( final SQLException e ) {
log.error( e );
}
finally {
Utils.close( rs );
}
}
/**
* Checks the files current in the index still are, if they are not then
* a IndexEvent.MISSING event is fired.
*
* If they appear changed then a IndexEvent.CHANGED event is fired.
*
*/
protected void checkIntegrity( final ScanFilter filter ) {
ResultSet rs = null;
try {
rs = getFiles();
while ( rs.next() ) {
final String path = rs.getString( "file_path" );
final int id = rs.getInt( "file_id" );
final File file = new File( path );
if ( checkExists(file, id) ) {
if ( checkModified( file, id, rs.getDate("index_last_modified"), filter ) ) {
markFileModified(id, rs.getInt("index_id"));
}
}
}
}
catch ( final SQLException e ) {
log.debug( e );
}
finally {
Utils.close( rs );
}
}
/**
* Updates the cache with the files we have indexed
*
* @throws java.sql.SQLException
*
*/
protected void updateCache() throws SQLException {
ResultSet rs = null;
try {
cache.clear();
rs = getFiles();
while ( rs.next() ) {
cache.add( rs.getString("file_path") );
}
}
finally {
Utils.close( rs );
}
}
/**
* Scans a directory for new files, but as the index requires the IndexCache
* to work properly runs an integrity check (should be fast)
*
* @param directoryId
* @param directory
*
*/
@Override
public void scanDirectory( final int directoryId, final File directory ) throws SQLException {
updateCache();
scan( directoryId, directory );
}
/**
* Scans a directory (and all sub-directories) for new files to index.
*
* This uses the IndexCache to check if files already exist. This will
* be populated with the results of the last index integrity scan.
*
* @param directoryId
* @param directory
*
*/
protected void scan( final int directoryId, final File directory ) {
if ( !directory.exists() || !directory.canRead() ) return; // make sure the directory exists
for ( final File file : directory.listFiles(getFileFilter()) ) {
if ( file.isDirectory() ) {
scan( directoryId, file );
}
else if ( !cache.exists(file.getAbsolutePath()) ) {
fireIndexEvent(
new IndexEvent( IndexEvent.Type.UNKNOWN, directoryId, file )
);
}
}
}
/**
* returns a ResultSet with the root directories to scan
*
* @return
*
* @throws java.sql.SQLException
*
*/
protected ResultSet getDirectories() throws SQLException {
final String sql = getDirectoriesSql();
final PreparedStatement st = db.prepare( sql );
return st.executeQuery();
}
/**
* Returns the contents of the index
*
* @return
*
* @throws java.sql.SQLException
*
*/
protected ResultSet getFiles() throws SQLException {
final String sql = getFilesSql();
final PreparedStatement st = db.prepare( sql );
return st.executeQuery();
}
/**
* marks a file in the index as being changed. if the file hasn't been indexed
* yet then the indexId should be -1
*
* @param id
*
* @return true if index updated, false otherwise
*
*/
public boolean markFileModified( final int fileId, final int indexId ) {
PreparedStatement st = null;
try {
String sql = "";
if ( indexId == 0 ) {
sql = " insert into indexer ( id, last_modified ) " +
" values ( ?, current_timestamp ) ";
st = db.prepare( sql );
st.setInt( 1, fileId );
}
else {
sql = " update indexer " +
" set last_modified = current_timestamp " +
" where id = ? ";
st = db.prepare( sql );
st.setInt( 1, indexId );
}
st.execute();
return true;
}
catch ( final SQLException e ) {
log.debug( e );
}
finally {
Utils.close( st );
}
return false;
}
/**
* Checks if an indexed file exists. If it doesn't then an IndexEvent.TRACK_MISSING
* event is fired and false returned.
*
* @param file
* @param id
*
* @return true if file exists, false otherwise
*
* @throws java.sql.SQLException
*
*/
protected boolean checkExists( final File file, final int id ) throws SQLException {
if ( !file.exists() ) {
fireIndexEvent(
new IndexEvent( IndexEvent.Type.MISSING, id, file )
);
return false;
}
return true;
}
/**
* Checks if a file has been modified. If it has then an IndexEvent.Type.CHANGED
* event is fired and true returned.
*
* @param file
* @param id
* @param lastModified
*
* @return true if file modified, false otherwise
*
* @throws java.sql.SQLException
*
*/
protected boolean checkModified( final File file, final int id, final Date lastModified, final ScanFilter filter ) {
boolean changed = false;
if ( ScanFilter.MODIFICATION_DATE.equals(filter) ) {
changed = (lastModified == null || lastModified.getTime() < file.lastModified());
} else if ( ScanFilter.NONE.equals(filter) ) {
changed = true;
}
if ( changed ) {
fireIndexEvent(new IndexEvent(IndexEvent.Type.CHANGED, id, file));
}
return changed;
}
/**
* Fires an IndexEvent to all listeners
*
* @param evt
*
*/
protected void fireIndexEvent( final IndexEvent evt ) {
log.debug( evt );
for ( IndexListener listener : indexListeners ) {
listener.indexChanged( evt );
}
}
/**
* Adds a listener for index events
*
* @param listener
*
*/
@Override
public void addIndexListener( final IndexListener listener ) {
indexListeners.add( listener );
}
}