/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: FileAdministration.java,v 1.39 2004/09/03 23:29:00 gregork Exp $
 */
package phex.share;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.TimerTask;

import javax.xml.bind.JAXBException;

import phex.common.Cfg;
import phex.common.Environment;
import phex.common.EnvironmentConstants;
import phex.common.RunnerQueueWorker;
import phex.common.ServiceManager;
import phex.common.ThreadTracking;
import phex.common.URN;
import phex.event.AsynchronousDispatcher;
import phex.event.SharedFilesChangeListener;
import phex.utils.FileUtils;
import phex.utils.IOUtil;
import phex.utils.Logger;
import phex.utils.ReadWriteLock;
import phex.utils.VersionUtils;
import phex.xml.ObjectFactory;
import phex.xml.XJBPhex;
import phex.xml.XJBSharedFile;
import phex.xml.XJBSharedLibrary;
import phex.xml.XMLBuilder;

import com.bitzi.util.Base32;
import com.bitzi.util.SHA1;

/**
 * A administrator of the shared files. It provides all necessary accessor
 * methods to administrate the shared files.
 */
public class FileAdministration
{
    // Alias file is not used.
    //private final static String aliasFile = "filealias";
    //private final static String aliasFilename = aliasFile + ".txt";

    private ReadWriteLock rwLock;

    /**
     * When accesseing this object locking via the rwLock object is required.
     */
    private ArrayList sharedDirectories;

    /**
     * This list contains the shared files without gaps. It is used for direct
     * and straight access via the getFileAt( position ). Also it is used for
     * getFileCount().
     * When accesseing this object locking via the rwLock object is required.
     */
    private ArrayList sharedFiles;

    /**
     * This lists holds all shared files at there current index position.
     * When files are un-shared during runtime a null is placed at the index
     * position of the removed file. Access via the file index is done using
     * the method getFileByIndex( fileIndex ).
     * When accesseing this object locking via the rwLock object is required.
     */
    private ArrayList indexedSharedFiles;

    /**
     * A maps that maps URNs to the file they belong to. This is for performant
     * searching by urn.
     * When accesseing this object locking via the rwLock object is required.
     */
    private HashMap urnToFileMap;

    /**
     * This map contains all absolute file paths as keys for the ShareFile
     * behind it.
     * When accesseing this object locking via the rwLock object is required.
     */
    private HashMap nameToFileMap;

    /**
     * When accesseing this object locking via the rwLock object is required.
     */
    private ArrayList fileExclusionList;

    /**
     * When accesseing this object locking via the rwLock object is required.
     */
    private ArrayList fileInclusionList;

    private int totalFileSizeKb;

    /**
     * if this thread is alive a rescan is running.
     */
    private Thread rescanThread;
    
    /**
     * A instance of a background runner queue to calculate
     * urns.
     */
    private RunnerQueueWorker urnCalculationRunner;

    /**
     * All listeners interested in events.
     */
    private ArrayList listenerList = new ArrayList( 2 );

    public FileAdministration( )
    {
        Environment.getInstance().scheduleTimerTask( 
            new FileRescanTimer(), FileRescanTimer.TIMER_PERIOD,
            FileRescanTimer.TIMER_PERIOD );
        
        urnCalculationRunner = new RunnerQueueWorker();
        rwLock = new ReadWriteLock();
        sharedDirectories = new ArrayList( 1 );
        sharedFiles = new ArrayList();
        indexedSharedFiles = new ArrayList();
        urnToFileMap = new HashMap();
        fileExclusionList = new ArrayList();
        fileInclusionList = new ArrayList();
        nameToFileMap = new HashMap();
        totalFileSizeKb = 0;
    }

    /**
     * Gets the file at the given index in the shared file list.
     * To access via the file index use the method getFileByIndex( fileIndex )
     */
    public ShareFile getFileAt( int index )
    {
        rwLock.readLock();
        try
        {
            if ( index >= sharedFiles.size() )
            {
                return null;
            }
            return (ShareFile) sharedFiles.get( index );
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    /**
     * Returns an array of all shared files.
     */
    public ShareFile[] getSharedFiles()
    {
        rwLock.readLock();
        try
        {
            ShareFile[] array = new ShareFile[ sharedFiles.size() ];
            array = (ShareFile[])sharedFiles.toArray( array );
            return array;
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    /**
     * Returns an array of all shared directories. A Object array is returned instead
     * of an shared file array for performence and memory reasons. This way no
     * array converting is necessary.
     */
    public File[] getSharedDirectories()
    {
        rwLock.readLock();
        try
        {
            File[] fileArr = new File[ sharedDirectories.size() ];
            sharedDirectories.toArray( fileArr );
            return fileArr;
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }
    
    public String getSharedFilePath( File file )
    {
        rwLock.readLock();
        try
        {
            Iterator iterator = sharedDirectories.iterator();
            while ( iterator.hasNext() )
            {
                File sharedDir = (File) iterator.next();
                if ( FileUtils.isChildOfDir( file, sharedDir ) )
                {
                    return file.getAbsolutePath().substring( sharedDir.getAbsolutePath().length() );
                }
            }
            return file.getName();
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    /**
     * Returns the shared files count.
     */
    public int getFileCount()
    {
        rwLock.readLock();
        try
        {
            return sharedFiles.size();
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    /**
     * Returns the a shared file by name. If the given name is null or a
     * file with this name is not found then null is returned.
     */
    public ShareFile getFileByName( String name )
    {
        rwLock.readLock();
        try
        {
            return (ShareFile) nameToFileMap.get( name );
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    /**
     * Returns a shared file by its index number.
     */
    public ShareFile getFileByIndex( int fileIndex )
        throws IndexOutOfBoundsException
    {
        rwLock.readLock();
        try
        {
            if ( fileIndex >= indexedSharedFiles.size() )
            {
                return null;
            }
            return (ShareFile) indexedSharedFiles.get( fileIndex );
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    /**
     * Returns a shared file by its urn. If the given urn is null or a
     * file with this URN is not found then null is returned.
     */
    public ShareFile getFileByURN( URN fileURN )
        throws IndexOutOfBoundsException
    {
        rwLock.readLock();
        try
        {
            if ( fileURN == null )
            {
                return null;
            }
            return (ShareFile) urnToFileMap.get( fileURN );
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }
    
    /**
     * Returns whether a file with the given URN is shared or not. 
     * @return true when a file with the given URN is shared, false otherwise.
     */
    public boolean isURNShared( URN fileURN )
        throws IndexOutOfBoundsException
    {
        rwLock.readLock();
        try
        {
            if ( fileURN == null )
            {
                return false;
            }
            return urnToFileMap.containsKey( fileURN );
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    public List getFilesByURNs( URN[] urns )
    {
        rwLock.readLock();
        try
        {
            ArrayList results = new ArrayList( urns.length );
            for( int i = 0; i < urns.length; i++ )
            {
                Object obj = urnToFileMap.get( urns[i] );
                if ( obj != null )
                {
                    results.add( obj );
                }
            }
            return results;
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    /**
     * Returns the total size of all shared files in KB.
     */
    public int getTotalFileSizeInKb()
    {
        rwLock.readLock();
        try
        {
            return totalFileSizeKb;
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    /**
     * Rescans the shared files. Locking is done in the called methods to
     * have gaps between a rescanning session.
     * You can specifiy if this is a initial rescan or not. On a initial
     * rescan all SharedFiles are dropped and the stored shared files infos are
     * loaded to access urn infos.
     */
    public void rescan( boolean isInitialRescan, boolean allowInterrupt )
    {
        if ( allowInterrupt && rescanThread != null && rescanThread.isAlive() )
        {
            // interrupt running thread to restart rescan...
            rescanThread.interrupt();
            // join interrupted thread and wait till its dead before
            // rescan starts
            try
            {
                rescanThread.join();
            }
            catch ( InterruptedException exp )
            {
                Logger.logWarning( exp );
            }
        }
        if ( rescanThread == null || !rescanThread.isAlive() )
        {
            FileRescanRunner runner = new FileRescanRunner( isInitialRescan );
            rescanThread = new Thread( ThreadTracking.rootThreadGroup, runner,
                "FileRescanRunner-" + Integer.toHexString( runner.hashCode() ) );
            rescanThread.setDaemon( true );
            rescanThread.setPriority( Thread.MIN_PRIORITY );
            rescanThread.start();
        }
    }

    /**
     * In case the user is sharing the download directory, skip the
     * download-in-progress files, index files and alias files. Even though
     * the user should not be able to configure the download directory as shared
     * directory since the new option dialog.
     */
    public boolean isFileInvalid( File file )
    {
        // In case the user is sharing the download directory,
        // skip the download-in-progress files.
        if (file.getName().toLowerCase().endsWith(".dl"))
        {
            return true;
        }
        // Skip PHEX generated index file.
        if (file.getName().equals("files.idx"))
        {
            return true;
        }
        // Skip alias file.
        // Alias file is not used...
        //if (file.getName().equals( aliasFilename ))
        //{
        //    return true;
        //}

        rwLock.readLock();
        try
        {
            if ( !isIncluded(file) )
            {
                return true;
            }
            if ( isExcluded(file) )
            {
                return true;
            }
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
        return false;
    }


    ////////////////// private methods ///////////////////////////////

    private XJBSharedLibrary loadSharedLibrary()
    {
        Logger.logMessage( Logger.FINE, Logger.UPLOAD,
            "Load shared library configuration file." );
        File file = Environment.getInstance().getPhexConfigFile(
            EnvironmentConstants.XML_SHARED_LIBRARY_FILE_NAME );

        XJBPhex phex;
        try
        {
            phex = XMLBuilder.loadXJBPhexFromFile( file );
            if ( phex == null )
            {
                Logger.logMessage( Logger.FINE, Logger.UPLOAD,
                    "No shared library configuration file found." );
                return ObjectFactory.createXJBSharedLibrary();
            }
        }
        catch ( JAXBException exp )
        {
            // TODO bring a GUI message that file cant be created
            Logger.logError( exp );
            try
            {
                return ObjectFactory.createXJBSharedLibrary();
            }
            catch ( JAXBException exp2 )
            {
                Logger.logError( exp2 );
                throw new RuntimeException( exp2.getMessage() );
            }
        }

        // update old download list
        XJBSharedLibrary sharedLibrary = phex.getSharedLibrary();
        return sharedLibrary;
    }

    public void saveSharedLibrary()
    {
        Logger.logMessage( Logger.FINEST, Logger.UPLOAD, "Saving shared library.");
        rwLock.readLock();
        try
        {
            // workaround for phex root... must be at a higher position for global use
            XJBPhex xphex = ObjectFactory.createPhexElement();

            XJBSharedLibrary library = createXJBSharedLibrary();
            xphex.setSharedLibrary( library );
            xphex.setPhexVersion( VersionUtils.getProgramVersion() );

            File file = Environment.getInstance().getPhexConfigFile(
                EnvironmentConstants.XML_SHARED_LIBRARY_FILE_NAME );
            XMLBuilder.saveToFile( file, xphex );
        }
        catch ( JAXBException exp )
        {
            // TODO bring a GUI message that file cant be created
            Throwable linkedException = exp.getLinkedException();
            if ( linkedException != null )
            {
                Logger.logError( linkedException );
            }
            Logger.logError( exp );
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    private XJBSharedLibrary createXJBSharedLibrary()
        throws JAXBException
    {
        XJBSharedLibrary library = ObjectFactory.createXJBSharedLibrary();
        rwLock.readLock();
        try
        {
            Iterator iterator = sharedFiles.iterator();
            List sharedFileList = library.getSharedFileList();
            while( iterator.hasNext() )
            {
                try
                {
                    ShareFile file = (ShareFile) iterator.next();
                    if ( file.getURN() == null )
                    {
                        continue;
                    }
                    XJBSharedFile xjbFile =  file.createXJBSharedFile();
                    sharedFileList.add( xjbFile );
                }
                catch ( Exception exp )
                {
                    Logger.logError( exp, "SharedFile skipped due to error." );
                }
            }
        }
        finally
        {
            try{ rwLock.readUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
        return library;
    }


    /**
     * Adds a shared file if its not already shared.
     * Its importent that the file owns a valid urn when being added.
     */
    private void addSharedFile( ShareFile shareFile )
    {
        File file = shareFile.getFile();
        // check if file is already there
        if ( getFileByName( file.getAbsolutePath() ) != null )
        {
            return;
        }
        
        rwLock.writeLock();
        int position;
        try
        {
            position = indexedSharedFiles.size();
            shareFile.setFileIndex( position );
            //shareFile.setAlias((String)aliasMapping.get(file.getName()));
            //addWordIndex(file.getName(), fileNumber, wordFiles);
            indexedSharedFiles.add( position, shareFile );

            position = sharedFiles.size();
            sharedFiles.add( position, shareFile );

            // dont add to urn map yet since urns get calculated in background.
            nameToFileMap.put( file.getAbsolutePath(), shareFile );
            totalFileSizeKb += file.length() / 1024;
        }
        finally
        {
            try{ rwLock.writeUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
        fireSharedFileAdded( position );
    }

    /**
     * Removed a shared file if its shared.
     */
    private void removeSharedFile( ShareFile shareFile )
    {
        rwLock.writeLock();
        int position;
        try
        {
            // clear index position...
            int fileIndex = shareFile.getFileIndex();
            indexedSharedFiles.set( fileIndex, null );

            // remove name to file map
            File file = shareFile.getFile();
            urnToFileMap.remove( shareFile.getURN() );
            nameToFileMap.remove( file.getAbsolutePath() );

            // try to find shareFile in access list
            position = sharedFiles.indexOf( shareFile );
            if ( position != -1 )
            {// if removed update data
                sharedFiles.remove( position );
                totalFileSizeKb -= shareFile.getFileSize() / 1024;
            }
        }
        finally
        {
            try{ rwLock.writeUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
        if ( position != -1 )
        {// if removed fire events
            fireSharedFileRemoved( position );
        }
    }

    /**
     * Clears the complete shared file list. Without making information
     * persistent.
     */
    private void clearSharedFiles()
    {
        rwLock.writeLock();
        try
        {
            urnCalculationRunner.stopAndClear();
            sharedFiles.clear();
            indexedSharedFiles.clear();
            urnToFileMap.clear();
            nameToFileMap.clear();
            totalFileSizeKb = 0;
        }
        finally
        {
            try{ rwLock.writeUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
        fireAllSharedFilesChanged();
    }

    private void setInclusions(String str)
    {
        StringTokenizer	tokens = new StringTokenizer(str, ";");
        fileInclusionList.clear();
        while (tokens.hasMoreTokens())
        {
            fileInclusionList.add(tokens.nextToken().trim().toLowerCase());
        }
    }

    private boolean isIncluded( File file )
    {
        // use filter only for files not directories
        if ( file.isDirectory() )
        {
            return true;
        }

        String name = file.getName().toLowerCase();

        for (int i = 0; i < fileInclusionList.size(); i++)
        {
            String	pattern = (String) fileInclusionList.get( i );

            if (pattern.equals("*"))
            {
                return true;
            }
            if (pattern.equals("*.*"))
            {
                return true;
            }

            // Take care of *exp and exp* cases.
            if (pattern.startsWith("*"))
            {
                pattern = pattern.substring(1);
            }
            if (pattern.endsWith("*"))
            {
                pattern = pattern.substring(0, pattern.length() - 1);
            }

            if (name.indexOf(pattern) != -1)
            {
                return true;
            }
        }
        return false;
    }

    private void setExclusions(String str)
    {
        StringTokenizer	tokens = new StringTokenizer(str, ";");
        fileExclusionList.clear();

        while (tokens.hasMoreTokens())
        {
            fileExclusionList.add(tokens.nextToken().trim().toLowerCase());
        }
    }

    private boolean isExcluded(File file)
    {
        // use filter only for files not directories
        if ( file.isDirectory() )
        {
            return false;
        }

        String name = file.getName().toLowerCase();

        // Exclude any file contain the "filealias" string.
        // alias file is not used
        //if (name.indexOf( aliasFile ) != -1)
        //{
        //    return true;
        //}

        for (int i = 0; i < fileExclusionList.size(); i++)
        {
            String pattern = (String) fileExclusionList.get( i );
            if (name.indexOf(pattern) != -1)
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Called when rescanning files
     */
    private void setSharedDirectories(String dirs)
    {
        rwLock.writeLock();
        try
        {
            StringTokenizer	tokens = new StringTokenizer(dirs, ";");
            int count = tokens.countTokens();

            sharedDirectories.clear();
            sharedDirectories.ensureCapacity( count );

            while (tokens.hasMoreTokens())
            {
                File dir = new File( tokens.nextToken().trim() );
                if ( !sharedDirectories.contains( dir ) )
                {
                    sharedDirectories.add( dir );
                }
            }
        }
        finally
        {
            try{ rwLock.writeUnlock(); }
            catch (IllegalAccessException exp )
            { Logger.logError( exp ); }
        }
    }

    ///////////////////// START event handling methods ////////////////////////
    public void addSharedFilesChangeListener( SharedFilesChangeListener listener )
    {
        listenerList.add( listener );
    }

    public void removeSharedFilesChangeListener( SharedFilesChangeListener listener )
    {
        listenerList.remove( listener );
    }

    private void fireSharedFileChanged( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                SharedFilesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (SharedFilesChangeListener)listeners[ i ];
                    listener.sharedFileChanged( position );
                }
            }
        });
    }

    private void fireSharedFileAdded( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                SharedFilesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (SharedFilesChangeListener)listeners[ i ];
                    listener.sharedFileAdded( position );
                }
            }
        });
    }

    private void fireSharedFileRemoved( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                SharedFilesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (SharedFilesChangeListener)listeners[ i ];
                    listener.sharedFileRemoved( position );
                }
            }
        });
    }

    private void fireAllSharedFilesChanged( )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                SharedFilesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (SharedFilesChangeListener)listeners[ i ];
                    listener.allSharedFilesChanged( );
                }
            }
        });
    }

    public void fireSharedFileChanged( ShareFile file )
    {
        int position = sharedFiles.indexOf( file );
        if ( position >= 0 )
        {
            fireSharedFileChanged( position );
        }
    }
    ///////////////////// END event handling methods ////////////////////////

    ///////////////////// START inner classes ///////////////////////////////

    private class FileRescanRunner implements Runnable
    {
        private boolean isInitialRescan;
        private HashMap sharedFilesCache;

        public FileRescanRunner( boolean isInitialRescan )
        {
            this.isInitialRescan = isInitialRescan;
        }

        public void run()
        {
            Logger.logMessage( Logger.FINEST, Logger.UPLOAD,
                "Staring file rescan (Initial: " + isInitialRescan + ")." );

            Cfg cfg = ServiceManager.sCfg;
            setSharedDirectories( cfg.mUploadDir );

            setExclusions( cfg.mUploadFileExclusions );
            setInclusions( cfg.mUploadFileInclusions );

            if ( rescanThread.isInterrupted() )
            {
                return;
            }

            if ( isInitialRescan )
            {
                clearSharedFiles();
                if ( rescanThread.isInterrupted() )
                {
                    return;
                }
                buildSharedFilesCache();
            }
            else
            {
                removeUnsharedFiles();
            }
            if ( rescanThread.isInterrupted() )
            {
                return;
            }

            HashMap scannedDirMap = new HashMap();


            rwLock.readLock();
            try
            {
                Iterator iterator = sharedDirectories.iterator();
                while( iterator.hasNext() )
                {
                    File dir = (File) iterator.next();
                    scanDir(dir, scannedDirMap,
                        ServiceManager.sCfg.mUploadScanRecursively );
                    if ( rescanThread.isInterrupted() )
                    {
                        return;
                    }
                }
            }
            finally
            {
                try{ rwLock.readUnlock(); }
                catch (IllegalAccessException exp )
                { Logger.logError( exp ); }
            }

            saveSharedLibrary();
        }

        private void buildSharedFilesCache()
        {
            sharedFilesCache = new HashMap();
            XJBSharedLibrary library = loadSharedLibrary();
            Iterator iterator = library.getSharedFileList().iterator();
            while ( iterator.hasNext() && !rescanThread.isInterrupted() )
            {
                XJBSharedFile cachedFile = (XJBSharedFile) iterator.next();
                sharedFilesCache.put( cachedFile.getFileName(), cachedFile );
            }
        }

        /**
         * Scans a directory for files to share.
         * @param dir the directory to scan.
         * @param scannedDirs the directorys already scanned. This is used to
         *        keep it from scanning continuously through of symbolicly
         *        linked directorys in unix systems. See sf bug #603736
         * @param recursive whether we scan this directory recursive or not
         */
        private void scanDir(File dir, HashMap scannedDirMap, boolean recursive)
        {
            // verify if dir was already scanned.
            String canonicalPath;
            try
            {
                canonicalPath = dir.getCanonicalPath();
            }
            catch ( IOException exp )
            {
                Logger.logWarning( exp );
                return;
            }
            if ( scannedDirMap.containsKey( canonicalPath ) )
            {// directory was already scanned...
                return;
            }
            else
            {// not scanned... now add it as scanned...
                scannedDirMap.put( canonicalPath, "" );
            }


            File[] files = dir.listFiles();

            if ( files == null )
            {
                Logger.logError( Logger.UPLOAD,
                    "'" + dir + "' is not a directory." );
                return;
            }

            //Properties aliasMapping = loadAliasFile(dir);

            /*
            Properties wordFiles = new Properties();
            if (!ServiceManager.sCfg.mIndexFiles)
            {
                try
                {
                    FileInputStream is = new FileInputStream(new File(dir, "files.idx"));
                    wordFiles.load(is);
                    is.close();
                }
                catch (IOException e)
                {
                    // e.printStackTrace();
                }
            }
            */
            for (int j = 0; j < files.length && !rescanThread.isInterrupted(); j++)
            {
                if ( isFileInvalid( files[j] ) )
                {
                    continue;
                }

                if ( files[j].isFile() )
                {
                    handleScannedFile( files[j] );
                }
                else if ( files[j].isDirectory() && recursive )
                {
                    scanDir(files[j], scannedDirMap, recursive);
                }
            }

            /*if (ServiceManager.sCfg.mIndexFiles)
            {
                try
                {
                    FileOutputStream	os = new FileOutputStream(new File(dir, "files.idx"));
                    wordFiles.save(os, "PHEX's Words To Files Index");
                    os.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }*/
        }

        private void handleScannedFile( File file )
        {
            /*if (ServiceManager.sCfg.mIndexFiles)
            {
                indexFile(wordFiles, file);
            }*/
            ShareFile shareFile;
            if ( isInitialRescan )
            {
                shareFile = new ShareFile( file );
                // Try to find cached file info
                XJBSharedFile xjbFile = (XJBSharedFile)sharedFilesCache.get(
                    file.getAbsolutePath() );
                if ( xjbFile != null &&
                     xjbFile.getLastModified() == file.lastModified() )
                {
                    shareFile.updateFromCache( xjbFile );
                }
                else
                {
                    UrnCalculationWorker worker = new UrnCalculationWorker(
                        shareFile);
                    urnCalculationRunner.add( worker );
                    // TODOTHEX
                    // String rootThex = shareFile.calculateTigerTree();

                    if ( rescanThread.isInterrupted() )
                    {
                        return;
                    }
                }
                addSharedFile( shareFile );
            }
            else
            {
                // try to find file in already existing share
                shareFile = getFileByName( file.getAbsolutePath() );
                if ( shareFile == null )
                {// create new file
                    shareFile = new ShareFile( file );
                    UrnCalculationWorker worker = new UrnCalculationWorker(
                        shareFile);
                    urnCalculationRunner.add( worker );
                    // TODOTHEX
					// String rootThex=shareFile.calculateTigerTree();

                    if ( rescanThread.isInterrupted() )
                    {
                        return;
                    }
                    addSharedFile( shareFile );
                }
            }
        }

        private void removeUnsharedFiles()
        {
            rwLock.readLock();
            try
            {
                for ( int i = sharedFiles.size() - 1;
                    i >= 0 && !rescanThread.isInterrupted(); i-- )
                {
                    ShareFile sharedFile = (ShareFile) sharedFiles.get( i );
                    File file = sharedFile.getFile();
                    if ( !isInSharedDirectory( file ) ||
                         !file.exists() )
                    {
                        removeSharedFile( sharedFile );
                    }
                }
            }
            finally
            {
                try{ rwLock.readUnlock(); }
                catch (IllegalAccessException exp )
                { Logger.logError( exp ); }
            }
        }

        private boolean isInSharedDirectory( File file )
        {
            Iterator iterator = sharedDirectories.iterator();
            while ( iterator.hasNext() )
            {
                File sharedDir = (File) iterator.next();
                if ( FileUtils.isChildOfDir( file, sharedDir ) )
                {
                    return true;
                }
            }
            return false;
        }
    }
    
    private class UrnCalculationWorker implements Runnable
    {
        private ShareFile shareFile;
        
        UrnCalculationWorker( ShareFile shareFile )
        {
            this.shareFile = shareFile;
        }
        
        public void run()
        {
            calculateURN();
            // add the urn to the map to share by urn
            urnToFileMap.put( shareFile.getURN(), shareFile );
            
            saveSharedLibrary();
        }
        
        /**
         * Calculates the URN of the file for HUGE support. This method can take
         * some time for large files. For URN calculation a SHA-1 digest is created
         * over the complete file and the SHA-1 digest is translated into a Base32
         * representation.
         */
        private void calculateURN()
        {
            int urnCalculationMode = ServiceManager.sCfg.urnCalculationMode;
            FileInputStream inStream = null;
            try
            {
                inStream = new FileInputStream( shareFile.getFile() );
                MessageDigest messageDigest = new SHA1();
                byte[] buffer = new byte[64 * 1024];
                int length;
                long start = System.currentTimeMillis();
                while ((length = inStream.read(buffer)) != -1)
                {
                    // TODO2 offer two file scan modes
                    long start2 = System.currentTimeMillis();
                    messageDigest.update(buffer, 0, length);
                    long end2 = System.currentTimeMillis();
                    try
                    {
                        Thread.sleep((end2 - start2) * urnCalculationMode);
                    }
                    catch (InterruptedException exp)
                    {
                        // reset interrupted flag
                        Thread.currentThread().interrupt();
                        return;
                    }
                }
                inStream.close();
                byte[] shaDigest = messageDigest.digest();
                long end = System.currentTimeMillis();
                URN urn = new URN("urn:sha1:" + Base32.encode(shaDigest));
                shareFile.setURN( urn );
                if ( Logger.isLevelTypeLogged( Logger.FINEST, Logger.UPLOAD ) )
                {
                    Logger.logMessage(Logger.FINEST, Logger.UPLOAD, "SHA1 time: "
                        + (end - start) + " size: " + shareFile.getFile().length());
                }
            }
            catch (IOException e)
            {// dont care... no urn could be calculated...
                Logger.logMessage(Logger.FINE, Logger.UPLOAD, e);
            }
            finally
            {
                IOUtil.closeQuietly( inStream );
            }
        }
    }
    
    private class FileRescanTimer extends TimerTask
    {
        // once per minute
        public static final long TIMER_PERIOD = 1000 * 60;

        public void run()
        {
            try
            {
                rescan( false, false );
            }
            catch ( Throwable th )
            {
                Logger.logError( th );
            }
        }
    }
    
}