package com.limegroup.gnutella; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.auth.ContentManager; import com.limegroup.gnutella.auth.ContentResponseData; import com.limegroup.gnutella.auth.ContentResponseObserver; import com.limegroup.gnutella.downloader.VerifyingFile; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.routing.QueryRouteTable; import com.limegroup.gnutella.settings.SharingSettings; import com.limegroup.gnutella.library.LibraryData; import com.limegroup.gnutella.util.CommonUtils; import com.limegroup.gnutella.util.FileUtils; import com.limegroup.gnutella.util.Function; import com.limegroup.gnutella.util.I18NConvert; import com.limegroup.gnutella.util.IntSet; import com.limegroup.gnutella.util.ProcessingQueue; import com.limegroup.gnutella.util.StringUtils; import com.limegroup.gnutella.util.Trie; import com.limegroup.gnutella.version.UpdateHandler; import com.limegroup.gnutella.xml.LimeXMLDocument; /** * The list of all shared files. Provides operations to add and remove * individual files, directory, or sets of directories. Provides a method to * efficiently query for files whose names contain certain keywords.<p> * * This class is thread-safe. */ public abstract class FileManager { private static final Log LOG = LogFactory.getLog(FileManager.class); /** The string used by Clip2 reflectors to index hosts. */ public static final String INDEXING_QUERY = " "; /** The string used by LimeWire to browse hosts. */ public static final String BROWSE_QUERY = "*.*"; /** Subdirectory that is always shared */ public static final File PROGRAM_SHARE; /** Subdirectory that also is always shared. */ public static final File PREFERENCE_SHARE; static { File forceShare = new File(".", ".NetworkShare").getAbsoluteFile(); try { forceShare = FileUtils.getCanonicalFile(forceShare); } catch(IOException ignored) {} PROGRAM_SHARE = forceShare; forceShare = new File(CommonUtils.getUserSettingsDir(), ".NetworkShare").getAbsoluteFile(); try { forceShare = FileUtils.getCanonicalFile(forceShare); } catch(IOException ignored) {} PREFERENCE_SHARE = forceShare; } private static final ProcessingQueue LOADER = new ProcessingQueue("FileManagerLoader"); /** * List of event listeners for FileManagerEvents. * LOCKING: listenerLock */ private volatile List eventListeners = Collections.EMPTY_LIST; private final Object listenerLock = new Object(); /********************************************************************** * LOCKING: obtain this's monitor before modifying this. **********************************************************************/ /** * All of the data for FileManager. */ private final LibraryData _data = new LibraryData(); /** * The list of complete and incomplete files. An entry is null if it * is no longer shared. * INVARIANT: for all i, _files[i]==null, or _files[i].index==i and either * _files[i]._path is in a shared directory with a shareable extension or * _files[i]._path is the incomplete directory if _files[i] is an IncompleteFileDesc. */ private List /* of FileDesc */ _files; /** * The total size of all complete files, in bytes. * INVARIANT: _filesSize=sum of all size of the elements of _files, * except IncompleteFileDescs, whose size may change at any time. */ private long _filesSize; /** * The number of complete files. * INVARIANT: _numFiles==number of elements of _files that are not null * and not IncompleteFileDescs. */ private int _numFiles; /** * The total number of files that are pending sharing. * (ie: awaiting hashing or being added) */ private int _numPendingFiles; /** * The total number of incomplete files. * INVARIANT: _numFiles + _numIncompleteFiles == the number of * elements of _files that are not null. */ private int _numIncompleteFiles; /** * The number of files that are forcibly shared over the network. * INVARIANT: _numFiles >= _numForcedFiles. */ private int _numForcedFiles; /** * An index that maps a <tt>File</tt> on disk to the * <tt>FileDesc</tt> holding it. * * INVARIANT: For all keys k in _fileToFileDescMap, * _files[_fileToFileDescMap.get(k).getIndex()].getFile().equals(k) * * Keys must be canonical <tt>File</tt> instances. */ private Map /* of File -> FileDesc */ _fileToFileDescMap; /** * A trie mapping keywords in complete filenames to the indices in _files. * Keywords are the tokens when the filename is tokenized with the * characters from DELIMITERS as delimiters. * * IncompleteFile keywords are NOT stored. * * INVARIANT: For all keys k in _keywordTrie, for all i in the IntSet * _keywordTrie.get(k), _files[i]._path.substring(k)!=-1. Likewise for all * i, for all k in _files[i]._path where _files[i] is not an * IncompleteFileDesc, _keywordTrie.get(k) contains i. */ private Trie /* String -> IntSet */ _keywordTrie; /** * A map of appropriately case-normalized URN strings to the * indices in _files. Used to make query-by-hash faster. * * INVARIANT: for all keys k in _urnMap, for all i in _urnMap.get(k), * _files[i].containsUrn(k). Likewise for all i, for all k in * _files[i].getUrns(), _urnMap.get(k) contains i. */ private Map /* URN -> IntSet */ _urnMap; /** * The set of file extensions to share, sorted by StringComparator. * INVARIANT: all extensions are lower case. */ private static Set /* of String */ _extensions; /** * A mapping whose keys are shared directories and any subdirectories * reachable through those directories. The value for any key is the set of * indices of all shared files in that directory. * * INVARIANT: for any key k with value v in _sharedDirectories, for all i in * v, _files[i]._path == k + _files[i]._name. * * Likewise, for all j s.t. _files[j] != null and !(_files[j] instanceof * IncompleteFileDesc), _sharedDirectories.get( _files[j]._path - * _files[j]._name).contains(j). Here "==" is shorthand for file path * comparison and "a-b" is short for string 'a' with suffix 'b' removed. * * INVARIANT: all keys in this are canonicalized files, sorted by a * FileComparator. * * Incomplete shared files are NOT stored in this data structure, but are * instead in the _incompletesShared IntSet. */ private Map /* of File -> IntSet */ _sharedDirectories; /** * A Set of shared directories that are completely shared. Files in these * directories are shared by default and will be shared unless the File is * listed in SharingSettings.FILES_NOT_TO_SHARE. */ private Set /* of File */ _completelySharedDirectories; /** * The IntSet for incomplete shared files. * * INVARIANT: for all i in _incompletesShared, * _files[i]._path == the incomplete directory. * _files[i] instanceof IncompleteFileDesc * Likewise, for all i s.t. * _files[i] != null and _files[i] instanceof IncompleteFileDesc, * _incompletesShared.contains(i) * * This structure is not strictly needed for correctness, but it allows * others to retrieve all the incomplete shared files, which is * relatively useful. */ private IntSet _incompletesShared; /** * A Set of URNs that we're currently requesting validation for. * This is NOT cleared on new revisions, because it'll always be * valid. */ private Set /* of URN */ _requestingValidation = Collections.synchronizedSet(new HashSet()); /** * The revision of the library. Every time 'loadSettings' is called, the revision * is incremented. */ protected volatile int _revision = 0; /** * The revision that finished loading all pending files. */ private volatile int _pendingFinished = -1; /** * The revision that finished updating shared directories. */ private volatile int _updatingFinished = -1; /** * If true, indicates that the FileManager is currently updating. */ private volatile boolean _isUpdating = false; /** * The last revision that finished both pending & updating. */ private volatile int _loadingFinished = -1; /** * Whether the FileManager has been shutdown. */ protected volatile boolean shutdown; /** * The filter object to use to discern shareable files. */ private final FileFilter SHAREABLE_FILE_FILTER = new FileFilter() { public boolean accept(File f) { return isFileShareable(f); } }; /** * The filter object to use to determine directories. */ private static final FileFilter DIRECTORY_FILTER = new FileFilter() { public boolean accept(File f) { return f.isDirectory(); } }; /** * An empty callback so we don't have to do != null checks everywhere. */ private static final FileEventListener EMPTY_CALLBACK = new FileEventListener() { public void handleFileEvent(FileManagerEvent evt) {} }; /** * The QueryRouteTable kept by this. The QueryRouteTable will be * lazily rebuilt when necessary. */ protected static QueryRouteTable _queryRouteTable; /** * Boolean for checking if the QRT needs to be rebuilt. */ protected static volatile boolean _needRebuild = true; /** * Characters used to tokenize queries and file names. */ public static final String DELIMITERS = " -._+/*()\\,"; private static final boolean isDelimiter(char c) { switch (c) { case ' ': case '-': case '.': case '_': case '+': case '/': case '*': case '(': case ')': case '\\': case ',': return true; default: return false; } } /** * Creates a new <tt>FileManager</tt> instance. */ public FileManager() { // We'll initialize all the instance variables so that the FileManager // is ready once the constructor completes, even though the // thread launched at the end of the constructor will immediately // overwrite all these variables resetVariables(); } /** * Method that resets all of the variables for this class, maintaining * all invariants. This is necessary, for example, when the shared * files are reloaded. */ private void resetVariables() { _filesSize = 0; _numFiles = 0; _numIncompleteFiles = 0; _numPendingFiles = 0; _numForcedFiles = 0; _files = new ArrayList(); _keywordTrie = new Trie(true); //ignore case _urnMap = new HashMap(); _extensions = new HashSet(); _sharedDirectories = new HashMap(); _completelySharedDirectories = new HashSet(); _incompletesShared = new IntSet(); _fileToFileDescMap = new HashMap(); } /** Asynchronously loads all files by calling loadSettings. Sets this's * callback to be "callback", and notifies "callback" of all file loads. * @modifies this * @see loadSettings */ public void start() { _data.clean(); cleanIndividualFiles(); loadSettings(); } public void stop() { save(); shutdown = true; } protected void save(){ _data.save(); UrnCache.instance().persistCache(); CreationTimeCache.instance().persistCache(); } /////////////////////////////////////////////////////////////////////////// // Accessors /////////////////////////////////////////////////////////////////////////// /** * Returns the size of all files, in <b>bytes</b>. Note that the largest * value that can be returned is Integer.MAX_VALUE, i.e., ~2GB. If more * bytes are being shared, returns this value. */ public int getSize() { return ByteOrder.long2int(_filesSize); } /** * Returns the number of files. * This number does NOT include incomplete files or forcibly shared network files. */ public int getNumFiles() { return _numFiles - _numForcedFiles; } /** * Returns the number of shared incomplete files. */ public int getNumIncompleteFiles() { return _numIncompleteFiles; } /** * Returns the number of pending files. */ public int getNumPendingFiles() { return _numPendingFiles; } /** * Returns the number of forcibly shared files. */ public int getNumForcedFiles() { return _numForcedFiles; } /** * Returns the file descriptor with the given index. Throws * IndexOutOfBoundsException if the index is out of range. It is also * possible for the index to be within range, but for this method to * return <tt>null</tt>, such as the case where the file has been * unshared. * * @param i the index of the <tt>FileDesc</tt> to access * @throws <tt>IndexOutOfBoundsException</tt> if the index is out of * range * @return the <tt>FileDesc</tt> at the specified index, which may * be <tt>null</tt> */ public synchronized FileDesc get(int i) { return (FileDesc)_files.get(i); } /** * Determines whether or not the specified index is valid. The index * is valid if it is within range of the number of files shared, i.e., * if:<p> * * i >= 0 && i < _files.size() <p> * * @param i the index to check * @return <tt>true</tt> if the index is within range of our shared * file data structure, otherwise <tt>false</tt> */ public synchronized boolean isValidIndex(int i) { return (i >= 0 && i < _files.size()); } /** * Returns the <tt>URN<tt> for the File. May return null; */ public synchronized URN getURNForFile(File f) { FileDesc fd = getFileDescForFile(f); if (fd != null) return fd.getSHA1Urn(); return null; } /** * Returns the <tt>FileDesc</tt> that is wrapping this <tt>File</tt> * or null if the file is not shared. */ public synchronized FileDesc getFileDescForFile(File f) { try { f = FileUtils.getCanonicalFile(f); } catch(IOException ioe) { return null; } return (FileDesc)_fileToFileDescMap.get(f); } /** * Determines whether or not the specified URN is shared in the library * as a complete file. */ public synchronized boolean isUrnShared(final URN urn) { FileDesc fd = getFileDescForUrn(urn); return fd != null && !(fd instanceof IncompleteFileDesc); } /** * Returns the <tt>FileDesc</tt> for the specified URN. This only returns * one <tt>FileDesc</tt>, even though multiple indices are possible with * HUGE v. 0.93. * * @param urn the urn for the file * @return the <tt>FileDesc</tt> corresponding to the requested urn, or * <tt>null</tt> if no matching <tt>FileDesc</tt> could be found */ public synchronized FileDesc getFileDescForUrn(final URN urn) { IntSet indices = (IntSet)_urnMap.get(urn); if(indices == null) return null; IntSet.IntSetIterator iter = indices.iterator(); //Pick the first non-null non-Incomplete FileDesc. FileDesc ret = null; while ( iter.hasNext() && ( ret == null || ret instanceof IncompleteFileDesc) ) { int index = iter.next(); ret = (FileDesc)_files.get(index); } return ret; } /** * Returns a list of all shared incomplete file descriptors. */ public synchronized FileDesc[] getIncompleteFileDescriptors() { if (_incompletesShared == null) return null; FileDesc[] ret = new FileDesc[_incompletesShared.size()]; IntSet.IntSetIterator iter = _incompletesShared.iterator(); for (int i = 0; iter.hasNext(); i++) { FileDesc fd = (FileDesc)_files.get(iter.next()); Assert.that(fd != null, "Directory has null entry"); ret[i]=fd; } return ret; } /** * Returns an array of all shared file descriptors. */ public synchronized FileDesc[] getAllSharedFileDescriptors() { // Instead of using _files.toArray, use // _fileToFileDescMap.values().toArray. This is because // _files will still contain null values for removed // shared files, but _fileToFileDescMap will not. FileDesc[] fds = new FileDesc[_fileToFileDescMap.size()]; fds = (FileDesc[])_fileToFileDescMap.values().toArray(fds); return fds; } /** * Returns a list of all shared file descriptors in the given directory, * in any order. * Returns null if directory is not shared, or a zero-length array if it is * shared but contains no files. This method is not recursive; files in * any of the directory's children are not returned. */ public synchronized FileDesc[] getSharedFileDescriptors(File directory) { if (directory == null) throw new NullPointerException("null directory"); // a. Remove case, trailing separators, etc. try { directory = FileUtils.getCanonicalFile(directory); } catch (IOException e) { // invalid directory ? return new FileDesc[0]; } //Lookup indices of files in the given directory... IntSet indices = (IntSet)_sharedDirectories.get(directory); if (indices == null) // directory not shared. return new FileDesc[0]; FileDesc[] fds = new FileDesc[indices.size()]; IntSet.IntSetIterator iter = indices.iterator(); for (int i = 0; iter.hasNext(); i++) { FileDesc fd = (FileDesc)_files.get(iter.next()); Assert.that(fd != null, "Directory has null entry"); fds[i] = fd; } return fds; } /////////////////////////////////////////////////////////////////////////// // Loading /////////////////////////////////////////////////////////////////////////// /** * Starts a new revision of the library, ensuring that only items present * in the appropriate sharing settings are shared. * * This method is non-blocking and thread-safe. * * @modifies this */ public void loadSettings() { final int currentRevision = ++_revision; if(LOG.isDebugEnabled()) LOG.debug("Starting new library revision: " + currentRevision); LOADER.add(new Runnable() { public void run() { loadStarted(currentRevision); loadSettingsInternal(currentRevision); } }); } /** * Loads the FileManager with a new list of directories. */ public void loadWithNewDirectories(Set shared) { SharingSettings.DIRECTORIES_TO_SHARE.setValue(shared); synchronized(_data.DIRECTORIES_NOT_TO_SHARE) { for(Iterator i = shared.iterator(); i.hasNext(); ) _data.DIRECTORIES_NOT_TO_SHARE.remove((File)i.next()); } RouterService.getFileManager().loadSettings(); } /** * Kicks off necessary stuff for a load being started. */ protected void loadStarted(int revision) { UrnCache.instance().clearPendingHashes(this); } /** * Notification that something finished loading. */ private void tryToFinish() { int revision; synchronized(this) { if(_pendingFinished != _updatingFinished || // Pending's revision must == update _pendingFinished != _revision || // The revision must be the current library's _loadingFinished >= _revision) // And we can't have already finished. return; _loadingFinished = _revision; revision = _loadingFinished; } loadFinished(revision); } /** * Kicks off necessary stuff for loading being done. */ protected void loadFinished(int revision) { if(LOG.isDebugEnabled()) LOG.debug("Finished loading revision: " + revision); // Various cleanup & persisting... trim(); CreationTimeCache.instance().pruneTimes(); RouterService.getDownloadManager().getIncompleteFileManager().registerAllIncompleteFiles(); save(); SavedFileManager.instance().run(); UpdateHandler.instance().tryToDownloadUpdates(); RouterService.getCallback().fileManagerLoaded(); } /** * Returns whether or not the loading is finished. */ public boolean isLoadFinished() { return _loadingFinished == _revision; } /** * Returns whether or not the updating is finished. */ public boolean isUpdating() { return _isUpdating; } /** * Loads all shared files, putting them in a queue for being added. * * If the current revision ever changed from the expected revision, this returns * immediately. */ protected void loadSettingsInternal(int revision) { if(LOG.isDebugEnabled()) LOG.debug("Loading Library Revision: " + revision); final File[] directories; synchronized (this) { // Reset the file list info resetVariables(); // Load the extensions. String[] extensions = StringUtils.split(SharingSettings.EXTENSIONS_TO_SHARE.getValue(), ";"); for(int i = 0; i < extensions.length; i++) _extensions.add(extensions[i].toLowerCase()); //Ideally we'd like to ensure that "C:\dir\" is loaded BEFORE //C:\dir\subdir. Although this isn't needed for correctness, it may //help the GUI show "subdir" as a subdirectory of "dir". One way of //doing this is to do a full topological sort, but that's a lot of //work. So we just approximate this by sorting by filename length, //from smallest to largest. Unless directories are specified as //"C:\dir\..\dir\..\dir", this will do the right thing. directories = SharingSettings.DIRECTORIES_TO_SHARE.getValueAsArray(); Arrays.sort(directories, new Comparator() { public int compare(Object a, Object b) { return (a.toString()).length()-(b.toString()).length(); } }); } //clear this, list of directories retrieved RouterService.getCallback().fileManagerLoading(); // Update the FORCED_SHARE directory. updateSharedDirectories(PROGRAM_SHARE, null, revision); updateSharedDirectories(PREFERENCE_SHARE, null, revision); //Load the shared directories and add their files. _isUpdating = true; for(int i = 0; i < directories.length && _revision == revision; i++) updateSharedDirectories(directories[i], null, revision); // Add specially shared files Set specialFiles = _data.SPECIAL_FILES_TO_SHARE; ArrayList list; synchronized(specialFiles) { // iterate over a copied list, since addFileIfShared might call // _data.SPECIAL_FILES_TO_SHARE.remove() which can cause a concurrent // modification exception list = new ArrayList(specialFiles); } for (Iterator i = list.iterator(); i.hasNext() && _revision == revision; ) addFileIfShared((File)i.next(), Collections.EMPTY_LIST, true, revision, null); _isUpdating = false; trim(); if(LOG.isDebugEnabled()) LOG.debug("Finished queueing shared files for revision: " + revision); _updatingFinished = revision; if(_numPendingFiles == 0) // if we didn't even try adding any files, pending is finished also. _pendingFinished = revision; tryToFinish(); } /** * Recursively adds this directory and all subdirectories to the shared * directories as well as queueing their files for sharing. Does nothing * if <tt>directory</tt> doesn't exist, isn't a directory, or has already * been added. This method is thread-safe. It acquires locks on a * per-directory basis. If the current revision ever changes from the * expected revision, this returns immediately. * * @requires directory is part of DIRECTORIES_TO_SHARE or one of its * children, and parent is directory's shared parent or null if * directory's parent is not shared. * @modifies this */ private void updateSharedDirectories(File directory, File parent, int revision) { // if(LOG.isDebugEnabled()) // LOG.debug("Attempting to share directory: " + directory); //We have to get the canonical path to make sure "D:\dir" and "d:\DIR" //are the same on Windows but different on Unix. try { directory = FileUtils.getCanonicalFile(directory); } catch (IOException e) { return; } // STEP 0: // Do not share certain the incomplete directory, directories on the // do not share list, or sensitive directories. if (directory.equals(SharingSettings.INCOMPLETE_DIRECTORY.getValue())) return; // Do not share directories on the do not share list if (_data.DIRECTORIES_NOT_TO_SHARE.contains(directory)) return; // Do not share sensitive directories if (isSensitiveDirectory(directory)) { // go through directories that explicitly should not be shared if (_data.SENSITIVE_DIRECTORIES_NOT_TO_SHARE.contains(directory)) return; // if we haven't already validated the sensitive directory, ask about it. if (_data.SENSITIVE_DIRECTORIES_VALIDATED.contains(directory)) { // ask the user whether the sensitive directory should be shared // THIS CALL CAN BLOCK. if (!RouterService.getCallback().warnAboutSharingSensitiveDirectory(directory)) return; } } // Exit quickly (without doing the dir lookup) if revisions changed. if(_revision != revision) return; // STEP 1: // Add directory boolean isForcedShare = isForcedShareDirectory(directory); synchronized (this) { // if it was already added, ignore. if (_completelySharedDirectories.contains(directory)) return; // if(LOG.isDebugEnabled()) // LOG.debug("Adding completely shared directory: " + directory); _completelySharedDirectories.add(directory); if (!isForcedShare) { dispatchFileEvent( new FileManagerEvent(this, FileManagerEvent.ADD_FOLDER, directory, parent)); } } // STEP 2: // Scan subdirectory for the amount of shared files. File[] file_list = directory.listFiles(SHAREABLE_FILE_FILTER); if (file_list == null) return; for(int i = 0; i < file_list.length && _revision == revision; i++) addFileIfShared(file_list[i], Collections.EMPTY_LIST, true, revision, null); // Exit quickly (without doing the dir lookup) if revisions changed. if(_revision != revision) return; // STEP 3: // Recursively add subdirectories. // This has the effect of ensuring that the number of pending files // is closer to correct number. // TODO: when we add non-recursive support, add it here. if (isForcedShare) return; // Do not share subdirectories of the forcibly shared dir. File[] dir_list = directory.listFiles(DIRECTORY_FILTER); for(int i = 0; i < dir_list.length && _revision == revision; i++) updateSharedDirectories(dir_list[i], directory, revision); } /////////////////////////////////////////////////////////////////////////// // Adding and removing shared files and directories /////////////////////////////////////////////////////////////////////////// /** * Removes a given directory from being completely shared. */ public void removeFolderIfShared(File folder) { _isUpdating = true; removeFolderIfShared(folder, null); _isUpdating = false; } /** * Removes a given directory from being completed shared. * If 'parent' is null, this will remove it from the root-level of * shared folders if it existed there. (If it is non-null & it was * a root-level shared folder, the folder remains shared.) * * The first time this is called, parent must be non-null in order to ensure * it works correctly. Otherwise, we'll end up adding tons of stuff * to the DIRECTORIES_NOT_TO_SHARE. */ protected void removeFolderIfShared(File folder, File parent) { if (!folder.isDirectory() && folder.exists()) throw new IllegalArgumentException("Expected a directory, but given: "+folder); try { folder = FileUtils.getCanonicalFile(folder); } catch(IOException ignored) {} // grab the value quickly. release the lock // so that we don't hold it during a long recursive function. // it's no big deal if it changes, we'll just do some extra work for a short // bit of time. boolean contained; synchronized(this) { contained = _completelySharedDirectories.contains(folder); } if(contained) { if(parent != null && SharingSettings.DIRECTORIES_TO_SHARE.contains(folder)) { // we don't wanna remove it, since it's a root-share, nor do we want // to remove any of its children, so we return immediately. return; } else if(parent == null) { if(!SharingSettings.DIRECTORIES_TO_SHARE.remove(folder)) _data.DIRECTORIES_NOT_TO_SHARE.add(folder); } // note that if(parent != null && not a root share) // we DO NOT ADD to DIRECTORIES_NOT_TO_SHARE. // this is by design, because the parent has already been removed // from sharing, which inherently will remove the child directories. // there's no need to clutter up DIRECTORIES_NOT_TO_SHARE with useless // entries. synchronized(this) { _completelySharedDirectories.remove(folder); } File[] subs = folder.listFiles(); if(subs != null) { for(int i = 0; i < subs.length; i++) { File f = subs[i]; if(f.isDirectory()) removeFolderIfShared(f, folder); else if(f.isFile() && !_data.SPECIAL_FILES_TO_SHARE.contains(f)) { if(removeFileIfShared(f) == null) UrnCache.instance().clearPendingHashesFor(f, this); } } } // send the event last. this is a hack so that the GUI can properly // receive events with the children first, moving any leftover children up to // potential parent directories. dispatchFileEvent( new FileManagerEvent(this, FileManagerEvent.REMOVE_FOLDER, folder)); } } /** * Adds a given folder to be shared. */ public void addSharedFolder(File folder) { if (!folder.isDirectory()) throw new IllegalArgumentException("Expected a directory, but given: "+folder); try { folder = FileUtils.getCanonicalFile(folder); } catch(IOException ignored) {} _data.DIRECTORIES_NOT_TO_SHARE.remove(folder); if (!isCompletelySharedDirectory(folder.getParentFile())) SharingSettings.DIRECTORIES_TO_SHARE.add(folder); _isUpdating = true; updateSharedDirectories(folder, null, _revision); _isUpdating = false; } /** * Always shares the given file. */ public void addFileAlways(File file) { addFileAlways(file, Collections.EMPTY_LIST, null); } /** * Always shares a file, notifying the given callback when shared. */ public void addFileAlways(File file, FileEventListener callback) { addFileAlways(file, Collections.EMPTY_LIST, callback); } /** * Always shares the given file, using the given list of metadata. */ public void addFileAlways(File file, List list) { addFileAlways(file, list, null); } /** * Adds the given file to share, with the given list of metadata, * even if it exists outside of what is currently accepted to be shared. * <p> * Too large files are still not shareable this way. * * The listener is notified if this file could or couldn't be shared. */ public void addFileAlways(File file, List list, FileEventListener callback) { _data.FILES_NOT_TO_SHARE.remove(file); if (!isFileShareable(file)) _data.SPECIAL_FILES_TO_SHARE.add(file); addFileIfShared(file, list, true, _revision, callback); } /** * Adds the given file if it's shared. */ public void addFileIfShared(File file) { addFileIfShared(file, Collections.EMPTY_LIST, true, _revision, null); } /** * Adds the given file if it's shared, notifying the given callback. */ public void addFileIfShared(File file, FileEventListener callback) { addFileIfShared(file, Collections.EMPTY_LIST, true, _revision, callback); } /** * Adds the file if it's shared, using the given list of metadata. */ public void addFileIfShared(File file, List list) { addFileIfShared(file, list, true, _revision, null); } /** * Adds the file if it's shared, using the given list of metadata, * informing the specified listener about the status of the sharing. */ public void addFileIfShared(File file, List list, FileEventListener callback) { addFileIfShared(file, list, true, _revision, callback); } /** * The actual implementation of addFileIfShared(File) * @param file the file to add * @param notify if true signals the front-end via * ActivityCallback.handleFileManagerEvent() about the Event */ protected void addFileIfShared(File file, List metadata, boolean notify, int revision, FileEventListener callback) { // if(LOG.isDebugEnabled()) // LOG.debug("Attempting to share file: " + file); if(callback == null) callback = EMPTY_CALLBACK; if(revision != _revision) { callback.handleFileEvent(new FileManagerEvent(this, FileManagerEvent.FAILED, file)); return; } // Make sure capitals are resolved properly, etc. try { file = FileUtils.getCanonicalFile(file); } catch (IOException e) { callback.handleFileEvent(new FileManagerEvent(this, FileManagerEvent.FAILED, file)); return; } synchronized(this) { if (revision != _revision) { callback.handleFileEvent(new FileManagerEvent(this, FileManagerEvent.FAILED, file)); return; } // if file is not shareable, also remove it from special files // to share since in that case it's not physically shareable then if (!isFileShareable(file)) { _data.SPECIAL_FILES_TO_SHARE.remove(file); callback.handleFileEvent(new FileManagerEvent(this, FileManagerEvent.FAILED, file)); return; } if(isFileShared(file)) { callback.handleFileEvent(new FileManagerEvent(this, FileManagerEvent.ALREADY_SHARED, file)); return; } _numPendingFiles++; // make sure _pendingFinished does not hold _revision // while we're still adding files _pendingFinished = -1; } UrnCache.instance().calculateAndCacheUrns(file, getNewUrnCallback(file, metadata, notify, revision, callback)); } /** * Constructs a new UrnCallback that will possibly load the file with the given URNs. */ protected UrnCallback getNewUrnCallback(final File file, final List metadata, final boolean notify, final int revision, final FileEventListener callback) { return new UrnCallback() { public void urnsCalculated(File f, Set urns) { // if(LOG.isDebugEnabled()) // LOG.debug("URNs calculated for file: " + f); FileDesc fd = null; synchronized(FileManager.this) { if(revision != _revision) { LOG.warn("Revisions changed, dropping share."); callback.handleFileEvent(new FileManagerEvent(FileManager.this, FileManagerEvent.FAILED, file)); return; } _numPendingFiles--; // Only load the file if we were able to calculate URNs and // the file is still shareable. if(!urns.isEmpty() && isFileShareable(file)) { fd = addFile(file, urns); } } if(fd != null) { loadFile(fd, file, metadata, urns); FileManagerEvent evt = new FileManagerEvent(FileManager.this, FileManagerEvent.ADD, fd); if(notify) // sometimes notify the GUI dispatchFileEvent(evt); callback.handleFileEvent(evt); // always notify the individual callback. } else { // If URNs was empty, or loading failed, notify... callback.handleFileEvent(new FileManagerEvent(FileManager.this, FileManagerEvent.FAILED, file)); } if(_numPendingFiles == 0) { _pendingFinished = revision; tryToFinish(); } } public boolean isOwner(Object o) { return o == FileManager.this; } }; } /** * Loads a single shared file. */ protected void loadFile(FileDesc fd, File file, List metadata, Set urns) { } /** * @requires the given file exists and is in a shared directory * @modifies this * @effects adds the given file to this if it is of the proper extension and * not too big (>~2GB). Returns true iff the file was actually added. * * @return the <tt>FileDesc</tt> for the new file if it was successfully * added, otherwise <tt>null</tt> */ private synchronized FileDesc addFile(File file, Set urns) { // if(LOG.isDebugEnabled()) // LOG.debug("Sharing file: " + file); int fileIndex = _files.size(); FileDesc fileDesc = new FileDesc(file, urns, fileIndex); ContentResponseData r = RouterService.getContentManager().getResponse(fileDesc.getSHA1Urn()); // if we had a response & it wasn't good, don't add this FD. if(r != null && !r.isOK()) return null; long fileLength = file.length(); _filesSize += fileLength; _files.add(fileDesc); _fileToFileDescMap.put(file, fileDesc); _numFiles++; //Register this file with its parent directory. File parent = file.getParentFile(); Assert.that(parent != null, "Null parent to \""+file+"\""); // Check if file is a specially shared file. If not, ensure that // it is located in a shared directory. IntSet siblings = (IntSet)_sharedDirectories.get(parent); if (siblings == null) { siblings = new IntSet(); _sharedDirectories.put(parent, siblings); } boolean added = siblings.add(fileIndex); Assert.that(added, "File "+fileIndex+" already found in "+siblings); // files that are forcibly shared over the network // aren't counted or shown. if(isForcedShareDirectory(parent)) _numForcedFiles++; //Index the filename. For each keyword... String[] keywords = extractKeywords(fileDesc); for (int i = 0; i < keywords.length; i++) { String keyword = keywords[i]; //Ensure the _keywordTrie has a set of indices associated with keyword. IntSet indices = (IntSet)_keywordTrie.get(keyword); if (indices == null) { indices = new IntSet(); _keywordTrie.add(keyword, indices); } //Add fileIndex to the set. indices.add(fileIndex); } // Commit the time in the CreactionTimeCache, but don't share // the installer. We populate free LimeWire's with free installers // so we have to make sure we don't influence the what is new // result set. if (!isForcedShare(file)) { URN mainURN = fileDesc.getSHA1Urn(); CreationTimeCache ctCache = CreationTimeCache.instance(); synchronized (ctCache) { Long cTime = ctCache.getCreationTime(mainURN); if (cTime == null) cTime = new Long(file.lastModified()); // if cTime is non-null but 0, then the IO subsystem is // letting us know that the file was FNF or an IOException // occurred - the best course of action is to // ignore the issue and not add it to the CTC, hopefully // we'll get a correct reading the next time around... if (cTime.longValue() > 0) { // these calls may be superfluous but are quite fast.... ctCache.addTime(mainURN, cTime.longValue()); ctCache.commitTime(mainURN); } } } // Ensure file can be found by URN lookups this.updateUrnIndex(fileDesc); _needRebuild = true; return fileDesc; } /** * Removes the file if it is being shared, and then removes the file from * the special lists as necessary. */ public synchronized void stopSharingFile(File file) { try { file = FileUtils.getCanonicalFile(file); } catch (IOException e) { return; } // remove file already here to heed against race conditions // wrt to filemanager events being handled on other threads boolean removed = _data.SPECIAL_FILES_TO_SHARE.remove(file); FileDesc fd = removeFileIfShared(file); if (fd == null) { UrnCache.instance().clearPendingHashesFor(file, this); } else { file = fd.getFile(); // if file was not specially shared, add it to files_not_to_share if (!removed) _data.FILES_NOT_TO_SHARE.add(file); } } /** * @modifies this * @effects ensures the first instance of the given file is not * shared. Returns FileDesc iff the file was removed. * In this case, the file's index will not be assigned to any * other files. Note that the file is not actually removed from * disk. */ public synchronized FileDesc removeFileIfShared(File f) { return removeFileIfShared(f, true); } /** * The actual implementation of removeFileIfShared(File) */ protected synchronized FileDesc removeFileIfShared(File f, boolean notify) { //Take care of case, etc. try { f = FileUtils.getCanonicalFile(f); } catch (IOException e) { return null; } // Look for matching file ... FileDesc fd = (FileDesc)_fileToFileDescMap.get(f); if (fd == null) return null; int i = fd.getIndex(); Assert.that(((FileDesc)_files.get(i)).getFile().equals(f), "invariant broken!"); _files.set(i, null); _fileToFileDescMap.remove(f); _needRebuild = true; // If it's an incomplete file, the only reference we // have is the URN, so remove that and be done. // We also return false, because the file was never really // "shared" to begin with. if (fd instanceof IncompleteFileDesc) { this.removeUrnIndex(fd); _numIncompleteFiles--; boolean removed = _incompletesShared.remove(i); Assert.that(removed, "File "+i+" not found in " + _incompletesShared); // Notify the GUI... if (notify) { FileManagerEvent evt = new FileManagerEvent(this, FileManagerEvent.REMOVE, fd ); dispatchFileEvent(evt); } return fd; } _numFiles--; _filesSize -= fd.getFileSize(); //Remove references to this from directory listing File parent = f.getParentFile(); IntSet siblings = (IntSet)_sharedDirectories.get(parent); Assert.that(siblings != null, "Removed file's directory \""+parent+"\" not in "+_sharedDirectories); boolean removed = siblings.remove(i); Assert.that(removed, "File "+i+" not found in "+siblings); // files that are forcibly shared over the network aren't counted if(isForcedShareDirectory(parent)) { notify = false; _numForcedFiles--; } //Remove references to this from index. String[] keywords = extractKeywords(fd); for (int j = 0; j < keywords.length; j++) { String keyword = keywords[j]; IntSet indices = (IntSet)_keywordTrie.get(keyword); if (indices != null) { indices.remove(i); if (indices.size() == 0) _keywordTrie.remove(keyword); } } //Remove hash information. this.removeUrnIndex(fd); //Remove creation time information if (_urnMap.get(fd.getSHA1Urn()) == null) CreationTimeCache.instance().removeTime(fd.getSHA1Urn()); // Notify the GUI... if (notify) { FileManagerEvent evt = new FileManagerEvent(this, FileManagerEvent.REMOVE, fd); dispatchFileEvent(evt); } return fd; } /** * Adds an incomplete file to be used for partial file sharing. * * @modifies this * @param incompleteFile the incomplete file. * @param urns the set of all known URNs for this incomplete file * @param name the completed name of this incomplete file * @param size the completed size of this incomplete file * @param vf the VerifyingFile containing the ranges for this inc. file */ public synchronized void addIncompleteFile(File incompleteFile, Set urns, String name, int size, VerifyingFile vf) { try { incompleteFile = FileUtils.getCanonicalFile(incompleteFile); } catch(IOException ioe) { //invalid file?... don't add incomplete file. return; } // We want to ensure that incomplete files are never added twice. // This may happen if IncompleteFileManager is deserialized before // FileManager finishes loading ... // So, every time an incomplete file is added, we check to see if // it already was... and if so, ignore it. // This is somewhat expensive, but it is called very rarely, so it's ok Iterator iter = urns.iterator(); while (iter.hasNext()) { // if there were indices for this URN, exit. IntSet shared = (IntSet)_urnMap.get(iter.next()); // nothing was shared for this URN, look at another if (shared == null) continue; for (IntSet.IntSetIterator isIter = shared.iterator(); isIter.hasNext(); ) { int i = isIter.next(); FileDesc desc = (FileDesc)_files.get(i); // unshared, keep looking. if (desc == null) continue; String incPath = incompleteFile.getAbsolutePath(); String path = desc.getFile().getAbsolutePath(); // the files are the same, exit. if (incPath.equals(path)) return; } } // no indices were found for any URN associated with this // IncompleteFileDesc... add it. int fileIndex = _files.size(); _incompletesShared.add(fileIndex); IncompleteFileDesc ifd = new IncompleteFileDesc( incompleteFile, urns, fileIndex, name, size, vf); _files.add(ifd); _fileToFileDescMap.put(incompleteFile, ifd); this.updateUrnIndex(ifd); _numIncompleteFiles++; _needRebuild = true; dispatchFileEvent(new FileManagerEvent(this, FileManagerEvent.ADD, ifd)); } /** * Notification that a file has changed and new hashes should be * calculated. */ public abstract void fileChanged(File f); /** Attempts to validate the given FileDesc. */ public void validate(final FileDesc fd) { ContentManager cm = RouterService.getContentManager(); if(_requestingValidation.add(fd.getSHA1Urn())) { cm.request(fd.getSHA1Urn(), new ContentResponseObserver() { public void handleResponse(URN urn, ContentResponseData r) { _requestingValidation.remove(fd.getSHA1Urn()); if(r != null && !r.isOK()) removeFileIfShared(fd.getFile()); } }, 5000); } } /////////////////////////////////////////////////////////////////////////// // Search, utility, etc... /////////////////////////////////////////////////////////////////////////// /** * @modifies this * @effects enters the given FileDesc into the _urnMap under all its * reported URNs */ private synchronized void updateUrnIndex(FileDesc fileDesc) { Iterator iter = fileDesc.getUrns().iterator(); while (iter.hasNext()) { URN urn = (URN)iter.next(); IntSet indices=(IntSet)_urnMap.get(urn); if (indices==null) { indices=new IntSet(); _urnMap.put(urn, indices); } indices.add(fileDesc.getIndex()); } } /** * Utility method to perform standardized keyword extraction for the given * <tt>FileDesc</tt>. This handles extracting keywords according to * locale-specific rules. * * @param fd the <tt>FileDesc</tt> containing a file system path with * keywords to extact * @return an array of keyword strings for the given file */ private static String[] extractKeywords(FileDesc fd) { return StringUtils.split(I18NConvert.instance().getNorm(fd.getPath()), DELIMITERS); } /** Removes any URN index information for desc */ private synchronized void removeUrnIndex(FileDesc fileDesc) { Iterator iter = fileDesc.getUrns().iterator(); while (iter.hasNext()) { //Lookup each of desc's URN's ind _urnMap. //(It better be there!) URN urn = (URN)iter.next(); IntSet indices=(IntSet)_urnMap.get(urn); Assert.that(indices!=null, "Invariant broken"); //Delete index from set. Remove set if empty. indices.remove(fileDesc.getIndex()); if (indices.size()==0) { RouterService.getAltlocManager().purge(urn); _urnMap.remove(urn); } } } /** * Renames a from from 'oldName' to 'newName'. */ public void renameFileIfShared(File oldName, File newName) { renameFileIfShared(oldName, newName, null); } /** * If oldName isn't shared, returns false. Otherwise removes "oldName", * adds "newName", and returns true iff newName is actually shared. The new * file may or may not have the same index as the original. * * This assumes that oldName has been deleted & newName exists now. * @modifies this */ public synchronized void renameFileIfShared(File oldName, final File newName, final FileEventListener callback) { FileDesc toRemove = getFileDescForFile(oldName); if (toRemove == null) { FileManagerEvent evt = new FileManagerEvent(this, FileManagerEvent.FAILED, oldName); dispatchFileEvent(evt); if(callback != null) callback.handleFileEvent(evt); return; } if(LOG.isDebugEnabled()) LOG.debug("Attempting to rename: " + oldName + " to: " + newName); List xmlDocs = new LinkedList(toRemove.getLimeXMLDocuments()); final FileDesc removed = removeFileIfShared(oldName, false); Assert.that(removed == toRemove, "invariant broken."); if (_data.SPECIAL_FILES_TO_SHARE.remove(oldName) && !isFileInCompletelySharedDirectory(newName)) _data.SPECIAL_FILES_TO_SHARE.add(newName); // Prepopulate the cache with new URNs. UrnCache.instance().addUrns(newName, removed.getUrns()); addFileIfShared(newName, xmlDocs, false, _revision, new FileEventListener() { public void handleFileEvent(FileManagerEvent evt) { if(LOG.isDebugEnabled()) LOG.debug("Add of newFile returned callback: " + evt); // Retarget the event for the GUI. FileManagerEvent newEvt = null; if(evt.isAddEvent()) { FileDesc fd = evt.getFileDescs()[0]; newEvt = new FileManagerEvent(FileManager.this, FileManagerEvent.RENAME, new FileDesc[]{removed,fd}); } else { newEvt = new FileManagerEvent(FileManager.this, FileManagerEvent.REMOVE, removed); } dispatchFileEvent(newEvt); if(callback != null) callback.handleFileEvent(newEvt); } }); } /** Ensures that this's index takes the minimum amount of space. Only * affects performance, not correctness; hence no modifies clause. */ private synchronized void trim() { _keywordTrie.trim(new Function() { public Object apply(Object intSet) { ((IntSet)intSet).trim(); return intSet; } }); } /** * Validates a file, moving it from 'SENSITIVE_DIRECTORIES_NOT_TO_SHARE' * to SENSITIVE_DIRECTORIES_VALIDATED'. */ public void validateSensitiveFile(File dir) { _data.SENSITIVE_DIRECTORIES_VALIDATED.add(dir); _data.SENSITIVE_DIRECTORIES_NOT_TO_SHARE.remove(dir); } /** * Invalidates a file, removing it from the shared directories, validated * sensitive directories, and adding it to the sensitive directories * not to share (so we don't ask again in the future). */ public void invalidateSensitiveFile(File dir) { _data.SENSITIVE_DIRECTORIES_VALIDATED.remove(dir); _data.SENSITIVE_DIRECTORIES_NOT_TO_SHARE.add(dir); SharingSettings.DIRECTORIES_TO_SHARE.remove(dir); } /** * Determines if there are any files shared that are not in completely shared directories. */ public boolean hasIndividualFiles() { return !_data.SPECIAL_FILES_TO_SHARE.isEmpty(); } /** * Returns all files that are shared while not in shared directories. */ public File[] getIndividualFiles() { Set candidates = _data.SPECIAL_FILES_TO_SHARE; synchronized(candidates) { ArrayList files = new ArrayList(candidates.size()); for(Iterator i = candidates.iterator(); i.hasNext(); ) { File f = (File)i.next(); if (f.exists()) files.add(f); } if (files.isEmpty()) return new File[0]; else return (File[])files.toArray(new File[files.size()]); } } /** * Determines if a given file is shared while not in a completely shared directory. */ public boolean isIndividualShare(File f) { return _data.SPECIAL_FILES_TO_SHARE.contains(f) && isFilePhysicallyShareable(f); } /** * Cleans all stale entries from the Set of individual files. */ private void cleanIndividualFiles() { Set files = _data.SPECIAL_FILES_TO_SHARE; synchronized(files) { for(Iterator i = files.iterator(); i.hasNext(); ) { Object o = i.next(); if(!(o instanceof File) || !(isFilePhysicallyShareable((File)o))) i.remove(); } } } /** * Returns true if the given file is shared by the FileManager. */ public boolean isFileShared(File file) { if (file == null) return false; if (_fileToFileDescMap.get(file) == null) return false; return true; } /** Returns true if file has a shareable extension. Case is ignored. */ private static boolean hasShareableExtension(File file) { if(file == null) return false; String filename = file.getName(); int begin = filename.lastIndexOf("."); if (begin == -1) return false; String ext = filename.substring(begin + 1).toLowerCase(); return _extensions.contains(ext); } /** * Returns true if this file is in a directory that is completely shared. */ public boolean isFileInCompletelySharedDirectory(File f) { File dir = f.getParentFile(); if (dir == null) return false; synchronized (this) { return _completelySharedDirectories.contains(dir); } } /** * Returns true if this dir is completely shared. */ public boolean isCompletelySharedDirectory(File dir) { if (dir == null) return false; synchronized (this) { return _completelySharedDirectories.contains(dir); } } /** * Returns true if the given file is in a completely shared directory * or if it is specially shared. */ private boolean isFileShareable(File file) { if (!isFilePhysicallyShareable(file)) return false; if (_data.SPECIAL_FILES_TO_SHARE.contains(file)) return true; if (_data.FILES_NOT_TO_SHARE.contains(file)) return false; if (isFileInCompletelySharedDirectory(file)) { if (file.getName().toUpperCase().startsWith("LIMEWIRE")) return true; if (!hasShareableExtension(file)) return false; return true; } return false; } /** * Returns true if this file is not too large, not too small, * not null, is a directory, can be read, is not hidden. Returns * true if file is a specially shared file or starts with "LimeWire". * Returns false otherwise. * @see isFileShareable(File) */ public static boolean isFilePhysicallyShareable(File file) { if (file == null || !file.exists() || file.isDirectory() || !file.canRead() || file.isHidden() ) return false; long fileLength = file.length(); if (fileLength > Integer.MAX_VALUE || fileLength <= 0) return false; return true; } /** * Returns true iff <tt>file</tt> is a sensitive directory. */ public static boolean isSensitiveDirectory(File file) { if (file == null) return false; // check for system roots File[] faRoots = File.listRoots(); if (faRoots != null && faRoots.length > 0) { for (int i = 0; i < faRoots.length; i++) { if (file.equals(faRoots[i])) return true; } } // check for user home directory String userHome = System.getProperty("user.home"); if (file.equals(new File(userHome))) return true; // check for OS-specific directories: if (CommonUtils.isWindows()) { // check for "Documents and Settings" if (file.getName().equals("Documents and Settings")) return true; // check for "My Documents" if (file.getName().equals("My Documents")) return true; // check for "Desktop" if (file.getName().equals("Desktop")) return true; // check for "Program Files" if (file.getName().equals("Program Files")) return true; // check for "Windows" if (file.getName().equals("Windows")) return true; // check for "WINNT" if (file.getName().equals("WINNT")) return true; } if (CommonUtils.isMacOSX()) { // check for /Users if (file.getName().equals("Users")) return true; // check for /System if (file.getName().equals("System")) return true; // check for /System Folder if (file.getName().equals("System Folder")) return true; // check for /Previous Systems if (file.getName().equals("Previous Systems")) return true; // check for /private if (file.getName().equals("private")) return true; // check for /Volumes if (file.getName().equals("Volumes")) return true; // check for /Desktop if (file.getName().equals("Desktop")) return true; // check for /Applications if (file.getName().equals("Applications")) return true; // check for /Applications (Mac OS 9) if (file.getName().equals("Applications (Mac OS 9)")) return true; // check for /Network if (file.getName().equals("Network")) return true; } if (CommonUtils.isPOSIX()) { // check for /bin if (file.getName().equals("bin")) return true; // check for /boot if (file.getName().equals("boot")) return true; // check for /dev if (file.getName().equals("dev")) return true; // check for /etc if (file.getName().equals("etc")) return true; // check for /home if (file.getName().equals("home")) return true; // check for /mnt if (file.getName().equals("mnt")) return true; // check for /opt if (file.getName().equals("opt")) return true; // check for /proc if (file.getName().equals("proc")) return true; // check for /root if (file.getName().equals("root")) return true; // check for /sbin if (file.getName().equals("sbin")) return true; // check for /usr if (file.getName().equals("usr")) return true; // check for /var if (file.getName().equals("var")) return true; } return false; } /** * Returns the QRTable. * If the shared files have changed, then it will rebuild the QRT. * A copy is returned so that FileManager does not expose * its internal data structure. */ public synchronized QueryRouteTable getQRT() { if(_needRebuild) { buildQRT(); _needRebuild = false; } QueryRouteTable qrt = new QueryRouteTable(_queryRouteTable.getSize()); qrt.addAll(_queryRouteTable); return qrt; } /** * build the qrt. Subclasses can add other Strings to the * QRT by calling buildQRT and then adding directly to the * _queryRouteTable variable. (see xml/MetaFileManager.java) */ protected synchronized void buildQRT() { _queryRouteTable = new QueryRouteTable(); FileDesc[] fds = getAllSharedFileDescriptors(); for(int i = 0; i < fds.length; i++) { if (fds[i] instanceof IncompleteFileDesc) continue; _queryRouteTable.add(fds[i].getPath()); } } ////////////////////////////////// Queries /////////////////////////////// /** * Constant for an empty <tt>Response</tt> array to return when there are * no matches. */ private static final Response[] EMPTY_RESPONSES = new Response[0]; /** * Returns an array of all responses matching the given request. If there * are no matches, the array will be empty (zero size). * * Incomplete Files are NOT returned in responses to queries. * * Design note: returning an empty array requires no extra allocations, * as empty arrays are immutable. */ public synchronized Response[] query(QueryRequest request) { String str = request.getQuery(); boolean includeXML = shouldIncludeXMLInResponse(request); //Special case: return up to 3 of your 'youngest' files. if (request.isWhatIsNewRequest()) return respondToWhatIsNewRequest(request, includeXML); //Special case: return everything for Clip2 indexing query (" ") and //browse queries ("*.*"). If these messages had initial TTLs too high, //StandardMessageRouter will clip the number of results sent on the //network. Note that some initial TTLs are filterd by GreedyQuery //before they ever reach this point. if (str.equals(INDEXING_QUERY) || str.equals(BROWSE_QUERY)) return respondToIndexingQuery(includeXML); //Normal case: query the index to find all matches. TODO: this //sometimes returns more results (>255) than we actually send out. //That's wasted work. //Trie requires that getPrefixedBy(String, int, int) passes //an already case-changed string. Both search & urnSearch //do this kind of match, so we canonicalize the case for them. str = _keywordTrie.canonicalCase(str); IntSet matches = search(str, null); if(request.getQueryUrns().size() > 0) matches = urnSearch(request.getQueryUrns().iterator(),matches); if (matches==null) return EMPTY_RESPONSES; List responses = new LinkedList(); final MediaType.Aggregator filter = MediaType.getAggregator(request); LimeXMLDocument doc = request.getRichQuery(); // Iterate through our hit indices to create a list of results. for (IntSet.IntSetIterator iter=matches.iterator(); iter.hasNext();) { int i = iter.next(); FileDesc desc = (FileDesc)_files.get(i); if(desc == null) Assert.that(false, "unexpected null in FileManager for query:\n"+ request); if ((filter != null) && !filter.allow(desc.getFileName())) continue; desc.incrementHitCount(); RouterService.getCallback().handleSharedFileUpdate(desc.getFile()); Response resp = new Response(desc); if(includeXML) { addXMLToResponse(resp, desc); if(doc != null && resp.getDocument() != null && !isValidXMLMatch(resp, doc)) continue; } responses.add(resp); } if (responses.size() == 0) return EMPTY_RESPONSES; return (Response[])responses.toArray(new Response[responses.size()]); } /** * Responds to a what is new request. */ private Response[] respondToWhatIsNewRequest(QueryRequest request, boolean includeXML) { // see if there are any files to send.... // NOTE: we only request up to 3 urns. we don't need to worry // about partial files because we don't add them to the cache. List urnList = CreationTimeCache.instance().getFiles(request, 3); if (urnList.size() == 0) return EMPTY_RESPONSES; // get the appropriate responses Response[] resps = new Response[urnList.size()]; for (int i = 0; i < urnList.size(); i++) { URN currURN = (URN) urnList.get(i); FileDesc desc = getFileDescForUrn(currURN); // should never happen since we don't add times for IFDs and // we clear removed files... if ((desc==null) || (desc instanceof IncompleteFileDesc)) throw new RuntimeException("Bad Rep - No IFDs allowed!"); // Formulate the response Response r = new Response(desc); if(includeXML) addXMLToResponse(r, desc); // Cache it resps[i] = r; } return resps; } /** Responds to a Indexing (mostly BrowseHost) query - gets all the shared * files of this client. */ private Response[] respondToIndexingQuery(boolean includeXML) { //Special case: if no shared files, return null // This works even if incomplete files are shared, because // they are added to _numIncompleteFiles and not _numFiles. if (_numFiles==0) return EMPTY_RESPONSES; //Extract responses for all non-null (i.e., not deleted) files. //Because we ignore all incomplete files, _numFiles continues //to work as the expected size of ret. Response[] ret=new Response[_numFiles-_numForcedFiles]; int j=0; for (int i=0; i<_files.size(); i++) { FileDesc desc = (FileDesc)_files.get(i); // If the file was unshared or is an incomplete file, // DO NOT SEND IT. if (desc==null || desc instanceof IncompleteFileDesc || isForcedShare(desc)) continue; Assert.that(j<ret.length, "_numFiles is too small"); ret[j] = new Response(desc); if(includeXML) addXMLToResponse(ret[j], desc); j++; } Assert.that(j==ret.length, "_numFiles is too large"); return ret; } /** * A normal FileManager will never include XML. * It is expected that MetaFileManager overrides this and returns * true in some instances. */ protected abstract boolean shouldIncludeXMLInResponse(QueryRequest qr); /** * This implementation does nothing. */ protected abstract void addXMLToResponse(Response res, FileDesc desc); /** * Determines whether we should include the response based on XML. */ protected abstract boolean isValidXMLMatch(Response res, LimeXMLDocument doc); /** * Returns a set of indices of files matching q, or null if there are no * matches. Subclasses may override to provide different notions of * matching. The caller of this method must not mutate the returned * value. */ protected IntSet search(String query, IntSet priors) { //As an optimization, we lazily allocate all sets in case there are no //matches. TODO2: we can avoid allocating sets when getPrefixedBy //returns an iterator of one element and there is only one keyword. IntSet ret=priors; //For each keyword in the query.... (Note that we avoid calling //StringUtils.split and take advantage of Trie's offset/limit feature.) for (int i=0; i<query.length(); ) { if (isDelimiter(query.charAt(i))) { i++; continue; } int j; for (j=i+1; j<query.length(); j++) { if (isDelimiter(query.charAt(j))) break; } //Search for keyword, i.e., keywords[i...j-1]. Iterator /* of IntSet */ iter= _keywordTrie.getPrefixedBy(query, i, j); if (iter.hasNext()) { //Got match. Union contents of the iterator and store in //matches. As an optimization, if this is the only keyword and //there is only one set returned, return that set without //copying. IntSet matches=null; while (iter.hasNext()) { IntSet s=(IntSet)iter.next(); if (matches==null) { if (i==0 && j==query.length() && !(iter.hasNext())) return s; matches=new IntSet(); } matches.addAll(s); } //Intersect matches with ret. If ret isn't allocated, //initialize to matches. if (ret==null) ret=matches; else ret.retainAll(matches); } else { //No match. Optimizaton: no matches for keyword => failure return null; } //Optimization: no matches after intersect => failure if (ret.size()==0) return null; i=j; } if (ret==null || ret.size()==0) return null; return ret; } /** * Find all files with matching full URNs */ private synchronized IntSet urnSearch(Iterator urnsIter,IntSet priors) { IntSet ret = priors; while(urnsIter.hasNext()) { URN urn = (URN)urnsIter.next(); IntSet hits = (IntSet)_urnMap.get(urn); if(hits!=null) { // double-check hits to be defensive (not strictly needed) IntSet.IntSetIterator iter = hits.iterator(); while(iter.hasNext()) { FileDesc fd = (FileDesc)_files.get(iter.next()); // If the file is unshared or an incomplete file // DO NOT SEND IT. if(fd == null || fd instanceof IncompleteFileDesc) continue; if(fd.containsUrn(urn)) { // still valid if(ret==null) ret = new IntSet(); ret.add(fd.getIndex()); } } } } return ret; } /** * Determines if this FileDesc is a network share. */ public static boolean isForcedShare(FileDesc desc) { return isForcedShare(desc.getFile()); } /** * Determines if this File is a network share. */ public static boolean isForcedShare(File file) { File parent = file.getParentFile(); return parent != null && isForcedShareDirectory(parent); } /** * Determines if this File is a network shared directory. */ public static boolean isForcedShareDirectory(File f) { return f.equals(PROGRAM_SHARE) || f.equals(PREFERENCE_SHARE); } /** * registers a listener for FileManagerEvents */ public void registerFileManagerEventListener(FileEventListener listener) { if (eventListeners.contains(listener)) return; synchronized (listenerLock) { List copy = new ArrayList(eventListeners); copy.add(listener); eventListeners = Collections.unmodifiableList(copy); } } /** * unregisters a listener for FileManagerEvents */ public void unregisterFileManagerEventListener(FileEventListener listener) { synchronized (listenerLock) { List copy = new ArrayList(eventListeners); copy.remove(listener); eventListeners = Collections.unmodifiableList(copy); } } /** * dispatches a FileManagerEvent to any registered listeners */ public void dispatchFileEvent(FileManagerEvent evt) { for (Iterator iter = eventListeners.iterator(); iter.hasNext();) { FileEventListener listener = (FileEventListener) iter.next(); listener.handleFileEvent(evt); } } }