package com.limegroup.gnutella.library; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.limewire.concurrent.ListeningFuture; import org.limewire.concurrent.ListeningFutureDelegator; import org.limewire.concurrent.SimpleFuture; import org.limewire.inspection.Inspectable; import org.limewire.listener.EventListener; import org.limewire.listener.SourcedEventMulticaster; import org.limewire.util.FileUtils; import org.limewire.util.Objects; import com.limegroup.gnutella.xml.LimeXMLDocument; /** * A List of FileDescs that are grouped together */ abstract class AbstractFileCollection extends AbstractFileView implements FileCollection, Inspectable { /** A list of listeners for this list */ private final SourcedEventMulticaster<FileViewChangeEvent, FileView> multicaster; /** The listener on the ManagedList, to synchronize changes. */ private final EventListener<FileViewChangeEvent> libraryListener; /** A rw lock. */ private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); /** The total size of all contained files. */ private volatile long totalFileSize = 0; public AbstractFileCollection(LibraryImpl library, SourcedEventMulticaster<FileViewChangeEvent, FileView> multicaster) { super(library); this.multicaster = multicaster; this.libraryListener = new LibrarySynchonizer(); } /** Initializes this list. Until the list is initialized, it is not valid. */ protected void initialize() { library.addListener(libraryListener); } @Override public ListeningFuture<List<ListeningFuture<FileDesc>>> addFolder(final File folder, final FileFilter fileFilter) { return library.scanFolderAndAddToCollection(folder, fileFilter, this); } @Override public ListeningFuture<FileDesc> add(File file) { if(!isFileAddable(file)) { return new SimpleFuture<FileDesc>(new FileViewChangeFailedException( file, FileViewChangeEvent.Type.FILE_ADD_FAILED, FileViewChangeFailedException.Reason.CANT_ADD_TO_LIST)); } FileDesc fd = library.getFileDesc(file); if(fd == null) { saveChange(canonicalize(file), true); // Save early, will RM if it can't become FD. return wrapFuture(library.add(file)); } else { add(fd); return futureFor(fd); } } @Override public ListeningFuture<FileDesc> add(File file, List<? extends LimeXMLDocument> documents) { if(!isFileAddable(file)) { return new SimpleFuture<FileDesc>(new FileViewChangeFailedException( file, FileViewChangeEvent.Type.FILE_ADD_FAILED, FileViewChangeFailedException.Reason.CANT_ADD_TO_LIST)); } FileDesc fd = library.getFileDesc(file); if(fd == null) { saveChange(canonicalize(file), true); // Save early, will RM if it can't become FD. return wrapFuture(library.add(file, documents)); } else { add(fd); return futureFor(fd); } } @Override public boolean add(FileDesc fileDesc) { if(!isFileAddable(fileDesc)) { return false; } if(addFileDescImpl(fileDesc)) { saveChange(fileDesc.getFile(), true); fireAddEvent(fileDesc); return true; } else { // Must rm from save-state, because we optimistically // inserted when adding as a file. if(!contains(fileDesc)) { saveChange(fileDesc.getFile(), false); } return false; } } /** * Performs the actual add. No notification is sent when this returns. * @return true if the fileDesc was added, false otherwise */ protected boolean addFileDescImpl(FileDesc fileDesc) { Objects.nonNull(fileDesc, "fileDesc"); rwLock.writeLock().lock(); try { if(isFileAddable(fileDesc) && getInternalIndexes().add(fileDesc.getIndex())) { totalFileSize += fileDesc.getFileSize(); return true; } else { return false; } } finally { rwLock.writeLock().unlock(); } } @Override public boolean remove(File file) { FileDesc fd = library.getFileDesc(file); if(fd != null) { return remove(fd); } else { saveChange(canonicalize(file), false); return false; } } @Override public boolean remove(FileDesc fileDesc) { saveChange(fileDesc.getFile(), false); if(removeFileDescImpl(fileDesc)) { fireRemoveEvent(fileDesc); return true; } else { return false; } } /** * Performs the actual remove. No notification is sent when this returns. * @return true if the fileDesc was removed, false otherwise */ protected boolean removeFileDescImpl(FileDesc fileDesc) { Objects.nonNull(fileDesc, "fileDesc"); rwLock.writeLock().lock(); try { if(getInternalIndexes().remove(fileDesc.getIndex())) { totalFileSize -= fileDesc.getFileSize(); return true; } else { return false; } } finally { rwLock.writeLock().unlock(); } } @Override public long getNumBytes() { return totalFileSize; } @Override public Iterator<FileDesc> iterator() { return new FileViewIterator(AbstractFileCollection.this, getInternalIndexes()); } @Override public Iterable<FileDesc> pausableIterable() { return new Iterable<FileDesc>() { @Override public Iterator<FileDesc> iterator() { return new ThreadSafeFileViewIterator(AbstractFileCollection.this); } }; } @Override public void clear() { clear(false); } /** Clears the list of files. If fromLibrary is true, the event is slightly different. */ private void clear(boolean fromLibrary) { boolean needsClearing; rwLock.writeLock().lock(); try { needsClearing = clearImpl(); } finally { rwLock.writeLock().unlock(); } if(needsClearing) { fireClearEvent(fromLibrary); } } /** Performs the actual clear -- returns true if anything was removed from this collection. */ protected boolean clearImpl() { boolean needsClearing = getInternalIndexes().size() > 0; getInternalIndexes().clear(); totalFileSize = 0; return needsClearing; } @Override public Object inspect() { rwLock.readLock().lock(); try { Map<String,Object> inspections = new HashMap<String,Object>(); inspections.put("num of files", Integer.valueOf(size())); inspections.put("size of all files", Long.valueOf(totalFileSize)); return inspections; } finally { rwLock.readLock().unlock(); } } @Override public Lock getReadLock() { return rwLock.readLock(); } @Override public void addListener(EventListener<FileViewChangeEvent> listener) { multicaster.addListener(this, listener); } @Override public boolean removeListener(EventListener<FileViewChangeEvent> listener) { return multicaster.removeListener(this, listener); } /** * Fires an addFileDesc event to all the listeners * @param fileDesc that was added */ protected void fireAddEvent(FileDesc fileDesc) { multicaster.broadcast(new FileViewChangeEvent(this, FileViewChangeEvent.Type.FILE_ADDED, fileDesc)); } /** * Fires a removeFileDesc event to all the listeners * @param fileDesc that was removed */ protected void fireRemoveEvent(FileDesc fileDesc) { multicaster.broadcast(new FileViewChangeEvent(this, FileViewChangeEvent.Type.FILE_REMOVED, fileDesc)); } /** * Fires a changeEvent to all the listeners * @param oldFileDesc FileDesc that was there previously * @param newFileDesc FileDesc that replaced oldFileDesc */ protected void fireChangeEvent(FileDesc oldFileDesc, FileDesc newFileDesc) { multicaster.broadcast(new FileViewChangeEvent(this, FileViewChangeEvent.Type.FILE_CHANGED, oldFileDesc, newFileDesc)); } /** Fires a meta-change event to all listeners */ protected void fireMetaChangeEvent(FileDesc fd) { multicaster.broadcast(new FileViewChangeEvent(this, FileViewChangeEvent.Type.FILE_META_CHANGED, fd)); } /** Fires a clear event to all listeners. */ protected void fireClearEvent(boolean fromLibrary) { multicaster.broadcast(new FileViewChangeEvent(this, FileViewChangeEvent.Type.FILES_CLEARED, fromLibrary)); } /** * Updates the list if a containing file has been renamed */ protected void updateFileDescs(FileDesc oldFileDesc, FileDesc newFileDesc) { boolean failed = false; boolean success = false; // Unfortunately cannot lock between these, since rm & add can be overridden // and the overridden methods cannot be expected to be OK with locks. if (removeFileDescImpl(oldFileDesc)) { if(addFileDescImpl(newFileDesc)) { // the internal LibraryFileData list is destroyed when a FILE_CHANGE occurs, // so resave ourselves to the list saveChange(newFileDesc.getFile(), true); success = true; } else { failed = true; } } // else nothing to remove -- neither success nor failure if(success) { fireChangeEvent(oldFileDesc, newFileDesc); } else if(failed) { // TODO: What do we want to do here? // This will have the side effect of causing ripples // if a rename/change fails for any reason. //saveChange(oldFileDesc.getFile(), false); fireRemoveEvent(oldFileDesc); } } /** Updates the list with new metadata about the file, possibly removing if it cannot be contained anymore. */ private void fileMetaChanged(FileDesc fd) { if(contains(fd)) { if(isFileAddable(fd)) { fireMetaChangeEvent(fd); } else { remove(fd); } } } /** * Returns true if this list is allowed to add this FileDesc * @param fileDesc - FileDesc to be added */ protected abstract boolean isFileAddable(FileDesc fileDesc); void dispose() { clear(); library.removeListener(libraryListener); multicaster.removeListeners(this); } /** * Returns true if a newly loaded file from the Managed List should be added to this list. * If FileDesc is non-null, it's the FileDesc that will be loaded. */ protected abstract boolean isPending(File file, FileDesc fileDesc); /** Hook for saving changes to data. */ protected abstract void saveChange(File file, boolean added); protected int getMaxIndex() { rwLock.readLock().lock(); try { return getInternalIndexes().max(); } finally { rwLock.readLock().unlock(); } } protected int getMinIndex() { rwLock.readLock().lock(); try { return getInternalIndexes().min(); } finally { rwLock.readLock().unlock(); } } private File canonicalize(File file) { try { return FileUtils.getCanonicalFile(file); } catch(IOException iox) { return file; } } private FileDesc throwExecutionExceptionIfNotContains(FileDesc fd) throws ExecutionException { if(contains(fd)) { return fd; } else { throw new ExecutionException(new FileViewChangeFailedException( fd.getFile(), FileViewChangeEvent.Type.FILE_ADD_FAILED, FileViewChangeFailedException.Reason.CANT_ADD_TO_LIST)); } } private ListeningFuture<FileDesc> wrapFuture(final ListeningFuture<FileDesc> future) { return new ListeningFutureDelegator<FileDesc, FileDesc>(future) { @Override protected FileDesc convertSource(FileDesc source) throws ExecutionException { return throwExecutionExceptionIfNotContains(source); } @Override protected FileDesc convertException(ExecutionException ee) throws ExecutionException { // We can fail because we attempted to add a File that already existed -- // if that's why we failed, then we return the file anyway (because it is added.) if(ee.getCause() instanceof FileViewChangeFailedException) { FileViewChangeFailedException fe = (FileViewChangeFailedException)ee.getCause(); if(fe.getType() == FileViewChangeEvent.Type.FILE_ADD_FAILED) { if(contains(fe.getFile())) { return getFileDesc(fe.getFile()); } } } throw ee; } }; } private ListeningFuture<FileDesc> futureFor(final FileDesc fd) { try { return new SimpleFuture<FileDesc>(throwExecutionExceptionIfNotContains(fd)); } catch(ExecutionException ee) { return new SimpleFuture<FileDesc>(ee); } } private class LibrarySynchonizer implements EventListener<FileViewChangeEvent> { @Override public void handleEvent(FileViewChangeEvent event) { // Note: We only need to check for pending on adds, // because that's the only kind that doesn't // require it already exists. switch(event.getType()) { case FILE_ADDED: if(isPending(event.getFile(), event.getFileDesc())) { add(event.getFileDesc()); } break; case FILE_META_CHANGED: fileMetaChanged(event.getFileDesc()); break; case FILE_CHANGED: updateFileDescs(event.getOldValue(), event.getFileDesc()); break; case FILE_REMOVED: remove(event.getFileDesc()); break; case FILES_CLEARED: clear(true); break; case FILE_CHANGE_FAILED: case FILE_ADD_FAILED: // This can fail for double-adds, meaning the FD really does exist. // If that's why it failed, we pretend this is really an add. FileDesc fd = library.getFileDesc(event.getFile()); if(fd == null) { // File doesn't exist, it was a real failure. if(isPending(event.getFile(), null) && !contains(event.getFile())) { saveChange(event.getFile(), false); } } else if(isPending(event.getFile(), fd)) { add(fd); } break; } } } }