package com.limegroup.gnutella.library; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.limewire.collection.IntSet; import org.limewire.collection.IntSet.IntSetIterator; import org.limewire.friend.api.Friend; import org.limewire.inject.EagerSingleton; import org.limewire.listener.EventListener; import org.limewire.listener.ListenerSupport; import org.limewire.listener.SourcedEventMulticaster; import org.limewire.listener.SourcedEventMulticasterImpl; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.util.StringUtils; import com.google.common.base.Predicate; import com.google.inject.Inject; import com.limegroup.gnutella.library.FileViewChangeEvent.Type; /** * The default implementation of {@link FileViewManager}. * * This uses {@link MultiFileView}s to represent a {@link FileView} backed by * multiple other views. Any mutable operation on the MultiFileView is performed * by this class, and this class is responsible for properly locking all mutable * operations. */ @EagerSingleton class FileViewManagerImpl implements FileViewManager { private static final Log LOG = LogFactory.getLog(FileViewManagerImpl.class); private final LibraryImpl library; /** Lock held to mutate any structure in this class or to mutate a MultiFileView. */ private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); /** A FileView containing every file that is shared with anyone. */ // We cannot construct it here because MultiFileView's constructor implicitly // uses FileViewManagerImpl.this.library, which might not be set until // the FileViewManagerImpl's constructor finishes. private final MultiFileView allSharedFilesView; /** * The share id mapped to the MultiFileView of all FileDescs visible by that id. * This is lazily built as people request views for a given id, which is why * scattered throughout this class places check for fileViewsPerFriend(id) != null. */ private final Map<String, MultiFileView> fileViewsPerFriend = new HashMap<String, MultiFileView>(); /** Any collection this is mapping to a MultiFileView. */ private final Collection<SharedFileCollection> sharedCollections = new ArrayList<SharedFileCollection>(); /** Multicaster used to broadcast events for a MultiFileView. */ private final SourcedEventMulticaster<FileViewChangeEvent, FileView> multicaster = new SourcedEventMulticasterImpl<FileViewChangeEvent, FileView>(); @Inject public FileViewManagerImpl(LibraryImpl library) { this.library = library; this.allSharedFilesView = new MultiFileView("All Shared Files"); } @Inject void register(ListenerSupport<FileViewChangeEvent> viewListeners, ListenerSupport<SharedFileCollectionChangeEvent> collectionListeners, FileCollectionManager collectionManager, ListenerSupport<FileDescChangeEvent> fileDescListeners) { for(SharedFileCollection collection : collectionManager.getSharedFileCollections()) { collectionAdded(collection); } viewListeners.addListener(new EventListener<FileViewChangeEvent>() { @Override public void handleEvent(FileViewChangeEvent event) { LOG.debugf("Handling event {0}", event); if(!(event.getSource() instanceof IncompleteFileCollection)) { switch(event.getType()) { case FILE_ADDED: if(isFileAddable(event.getFileDesc())) { fileAddedToCollection(event.getFileDesc(), (SharedFileCollection)event.getFileView(), false); } break; case FILE_REMOVED: fileRemovedFromCollection(event.getFileDesc(), (SharedFileCollection)event.getFileView(), false); break; case FILES_CLEARED: if(event.isLibraryClear()) { clearAllViews(); } else { collectionCleared((SharedFileCollection)event.getFileView()); } break; case FILE_CHANGED: fileChangedInCollection(event.getFileDesc(), event.getOldValue(), (SharedFileCollection)event.getFileView()); break; case FILE_META_CHANGED: fileMetaChangedInCollection(event.getFileDesc(), (SharedFileCollection)event.getFileView()); break; } } } }); collectionListeners.addListener(new EventListener<SharedFileCollectionChangeEvent>() { @Override public void handleEvent(SharedFileCollectionChangeEvent event) { LOG.debugf("Handling event {0}", event); switch(event.getType()) { case COLLECTION_ADDED: collectionAdded(event.getSource()); break; case COLLECTION_REMOVED: collectionRemoved(event.getSource()); break; case FRIEND_ADDED: friendAddedToCollection(event.getSource(), event.getFriendId()); break; case FRIEND_REMOVED: friendRemovedFromCollection(event.getSource(), event.getFriendId()); break; case FRIEND_IDS_CHANGED: friendIdsChangedInCollection(event.getSource(), event.getOldFriendIds(), event.getNewFriendIds()); break; } } }); } @Override public void addListener(EventListener<FileViewChangeEvent> listener) { multicaster.addListener(listener); } @Override public boolean removeListener(EventListener<FileViewChangeEvent> listener) { return multicaster.removeListener(listener); } FileView getAllSharedFilesView() { return allSharedFilesView; } FileView getGnutellaFileView() { return getFileViewForId(Friend.P2P_FRIEND_ID); } /** * Adds a new list of K => List<V> element to the map if v is not empty, * creating the map if it doesn't already exist. */ private <K, V> Map<K, List<V>> addToOrCreateMapOfList(Map<K, List<V>> map, K k, List<V> v) { if(!v.isEmpty()) { if(map == null) { map = new HashMap<K, List<V>>(); } map.put(k, v); } return map; } /** * Notification that a new collection was created. This looks at all the * share ids the collection is shared with any adds itself as a backing view * to any {@link MultiFileView}s that are mapped by that id. * * An event will be triggered for each {@link FileDesc} that was added to * each {@link MultiFileView}. */ private void collectionAdded(SharedFileCollection collection) { LOG.debugf("New collection {0} added", collection); Map<FileView, List<FileDesc>> addedFiles = null; rwLock.writeLock().lock(); try { sharedCollections.add(collection); List<String> friendList = collection.getFriendList(); // if it was shared with atleast one person, add it to the 'all shared' view. if(!friendList.isEmpty()) { List<FileDesc> added = allSharedFilesView.addNewBackingView(collection); addedFiles = addToOrCreateMapOfList(addedFiles, allSharedFilesView, added); } for(String id : friendList) { MultiFileView view = fileViewsPerFriend.get(id); if(view != null) { List<FileDesc> added = view.addNewBackingView(collection); LOG.debugf("Added collection {0} to view {1}, added {2}", collection, view, added); addedFiles = addToOrCreateMapOfList(addedFiles, view, added); } } } finally { rwLock.writeLock().unlock(); } if(addedFiles != null) { for(Map.Entry<FileView, List<FileDesc>> entry : addedFiles.entrySet()) { for(FileDesc fd : entry.getValue()) { multicaster.broadcast(new FileViewChangeEvent(entry.getKey(), Type.FILE_ADDED, fd)); } } } } /** * Notification that a collection was removed. This will remove the * collection as a backing view for any {@link MultiFileView}s that were * mapped to by any share ids the collection is shared with. * * An event will be sent for each {@link FileDesc} that was removed from * each {@link MultiFileView}. */ private void collectionRemoved(SharedFileCollection collection) { Map<FileView, List<FileDesc>> removedFiles = null; rwLock.writeLock().lock(); try { sharedCollections.remove(collection); List<FileDesc> removed = allSharedFilesView.removeBackingView(collection); removedFiles = addToOrCreateMapOfList(removedFiles, allSharedFilesView, removed); for(MultiFileView view : fileViewsPerFriend.values()) { removed = view.removeBackingView(collection); LOG.debugf("Removed collection {0} from view {1}, added {2}", collection, view, removed); removedFiles = addToOrCreateMapOfList(removedFiles, view, removed); } } finally { rwLock.writeLock().unlock(); } if(removedFiles != null) { for(Map.Entry<FileView, List<FileDesc>> entry : removedFiles.entrySet()) { for(FileDesc fd : entry.getValue()) { multicaster.broadcast(new FileViewChangeEvent(entry.getKey(), Type.FILE_REMOVED, fd)); } } } } /** * Notification that a collection is shared with another person. * * This will add the collection as a backing view for the * {@link MultiFileView} that exists for this id, if any view exists. * * An event will be sent for each {@link FileDesc} that was added to the * {@link MultiFileView}. */ private void friendAddedToCollection(SharedFileCollection collection, String id) { Map<FileView, List<FileDesc>> addedFiles = null; rwLock.writeLock().lock(); try { // make sure it's added to the 'all' view -- // if the all view already contained it, this will return // an empty list. List<FileDesc> added = allSharedFilesView.addNewBackingView(collection); addedFiles = addToOrCreateMapOfList(addedFiles, allSharedFilesView, added); MultiFileView view = fileViewsPerFriend.get(id); if(view != null) { added = view.addNewBackingView(collection); LOG.debugf("Friend {0} added to collection {1}, changing view {2}, added {3}", id, collection, view, added); addedFiles = addToOrCreateMapOfList(addedFiles, view, added); } } finally { rwLock.writeLock().unlock(); } if(addedFiles != null) { for(Map.Entry<FileView, List<FileDesc>> entry : addedFiles.entrySet()) { for(FileDesc fd : entry.getValue()) { multicaster.broadcast(new FileViewChangeEvent(entry.getKey(), Type.FILE_ADDED, fd)); } } } } /** * Notification that a collection is no longer shared with a person. * * This will remove the collection as a backing view from the * {@link MultiFileView} for that id, if any view exists. * * An event will be sent for each {@link FileDesc} that was removed from the * {@link MultiFileView}. */ private void friendRemovedFromCollection(SharedFileCollection collection, String id) { Map<FileView, List<FileDesc>> removedFiles = null; rwLock.writeLock().lock(); try { // if there are no more friends this collection is shared with, // remove it from the 'all' view. if(collection.getFriendList().isEmpty()) { List<FileDesc> removed = allSharedFilesView.removeBackingView(collection); removedFiles = addToOrCreateMapOfList(removedFiles, allSharedFilesView, removed); } MultiFileView view = fileViewsPerFriend.get(id); if(view != null) { List<FileDesc> removed = view.removeBackingView(collection); LOG.debugf("Friend {0} removed from collection {1}, changing view {2}, removed {2}", id, collection, view, removed); removedFiles = addToOrCreateMapOfList(removedFiles, view, removed); } } finally { rwLock.writeLock().unlock(); } if(removedFiles != null) { for(Map.Entry<FileView, List<FileDesc>> entry : removedFiles.entrySet()) { for(FileDesc fd : entry.getValue()) { multicaster.broadcast(new FileViewChangeEvent(entry.getKey(), Type.FILE_REMOVED, fd)); } } } } /** * Notification that a collection's set of shared friends changed. * * This will add the collection as a backing view for the * {@link MultiFileView} that exists for the new ids and remove the * collection as a backing view for any old ids. * * An event will be sent for each {@link FileDesc} that was added or removed from * the views. */ private void friendIdsChangedInCollection(SharedFileCollection collection, Collection<String> oldIds, Collection<String> newIds) { Map<FileView, List<FileDesc>> addedFiles = null; Map<FileView, List<FileDesc>> removedFiles = null; rwLock.writeLock().lock(); try { if(newIds.isEmpty()) { // if new is empty, must remove files from global list. List<FileDesc> removed = allSharedFilesView.removeBackingView(collection); removedFiles = addToOrCreateMapOfList(removedFiles, allSharedFilesView, removed); } else if(oldIds.isEmpty()) { // if old was empty, must add files to global list. List<FileDesc> added = allSharedFilesView.addNewBackingView(collection); addedFiles = addToOrCreateMapOfList(addedFiles, allSharedFilesView, added); } // Add any ids that were added. List<String> addedFriends = new ArrayList<String>(newIds); addedFriends.removeAll(oldIds); for(String id : addedFriends) { MultiFileView view = fileViewsPerFriend.get(id); if(view != null) { List<FileDesc> added = view.addNewBackingView(collection); LOG.debugf("Friend {0} added to collection {1}, changing view {2}, added {3}", id, collection, view, added); addedFiles = addToOrCreateMapOfList(addedFiles, view, added); } } // Remove any ids that were removed. List<String> removedFriends = new ArrayList<String>(oldIds); removedFriends.removeAll(newIds); for(String id : removedFriends) { MultiFileView view = fileViewsPerFriend.get(id); if(view != null) { List<FileDesc> removed = view.removeBackingView(collection); LOG.debugf("Friend {0} removed from collection {1}, changing view {2}, removed {2}", id, collection, view, removed); removedFiles = addToOrCreateMapOfList(removedFiles, view, removed); } } } finally { rwLock.writeLock().unlock(); } if(addedFiles != null) { for(Map.Entry<FileView, List<FileDesc>> entry : addedFiles.entrySet()) { for(FileDesc fd : entry.getValue()) { multicaster.broadcast(new FileViewChangeEvent(entry.getKey(), Type.FILE_ADDED, fd)); } } } if(removedFiles != null) { for(Map.Entry<FileView, List<FileDesc>> entry : removedFiles.entrySet()) { for(FileDesc fd : entry.getValue()) { multicaster.broadcast(new FileViewChangeEvent(entry.getKey(), Type.FILE_REMOVED, fd)); } } } } /** Notification that the library was cleared, and we need to clean all our views. */ private void clearAllViews() { List<FileView> clearedViews = new ArrayList<FileView>(); rwLock.writeLock().lock(); try { if(allSharedFilesView.size() > 0) { allSharedFilesView.clear(); clearedViews.add(allSharedFilesView); } for(MultiFileView view : fileViewsPerFriend.values()) { if(view.size() > 0) { view.clear(); clearedViews.add(view); } } } finally { rwLock.writeLock().unlock(); } for(FileView view : clearedViews) { multicaster.broadcast(new FileViewChangeEvent(view, Type.FILES_CLEARED, true)); } } /** * Notification that a particular collection was cleared. * @param collection */ private void collectionCleared(SharedFileCollection collection) { Map<FileView, List<FileDesc>> removedFiles = null; rwLock.writeLock().lock(); try { List<FileDesc> removed = allSharedFilesView.fileViewCleared(collection); removedFiles = addToOrCreateMapOfList(removedFiles, allSharedFilesView, removed); LOG.debugf("Collection cleared {0}, changing all view, removed {1}", collection, removed); for(String id : collection.getFriendList()) { MultiFileView view = fileViewsPerFriend.get(id); if(view != null) { removed = view.fileViewCleared(collection); LOG.debugf("Cleared collection {0}, changing view {1}, removed {2}", collection, view, removed); removedFiles = addToOrCreateMapOfList(removedFiles, view, removed); } } } finally { rwLock.writeLock().unlock(); } if(removedFiles != null) { for(Map.Entry<FileView, List<FileDesc>> entry : removedFiles.entrySet()) { for(FileDesc fd : entry.getValue()) { multicaster.broadcast(new FileViewChangeEvent(entry.getKey(), Type.FILE_REMOVED, fd)); } } } } /** * Notification that a file was removed from a shared collection. * * If that collection was shared with any friends and views are created for * those friends, then the file is attempted to be removed from the view. * The file will only be removed from the view if it was only shared * through this collection (if it was also shared through another collection * with the same friend, the file is not removed from the view). * * If forceRemoval is true, this will remove the file from any views this backed, * regardless of if the view is backed by other collections that may have * the file. */ private void fileRemovedFromCollection(FileDesc fileDesc, SharedFileCollection collection, boolean forceRemoval) { List<FileView> removedViews = null; rwLock.writeLock().lock(); try { if(allSharedFilesView.fileRemovedFromView(fileDesc, collection, forceRemoval)) { removedViews = new ArrayList<FileView>(); removedViews.add(allSharedFilesView); } for(String id : collection.getFriendList()) { MultiFileView view = fileViewsPerFriend.get(id); if(view != null) { if(view.fileRemovedFromView(fileDesc, collection, forceRemoval)) { if(removedViews == null) { removedViews = new ArrayList<FileView>(); } removedViews.add(view); LOG.debugf("File {0} removed from collection {1}, changing view {2}", fileDesc, collection, view); } else { if(LOG.isDebugEnabled()) { LOG.debugf("File {0} removed from collection {1}, but didn't change view {2}. View contains file? {3}", fileDesc, collection, view, view.contains(fileDesc)); } } } } } finally { rwLock.writeLock().unlock(); } if(removedViews != null) { for(FileView view : removedViews) { multicaster.broadcast(new FileViewChangeEvent(view, Type.FILE_REMOVED, fileDesc)); } } } /** * Essentially does a combination of * {@link #fileRemovedFromCollection(FileDesc, SharedFileCollection)} & * {@link #fileAddedToCollection(FileDesc, SharedFileCollection)}, except it * sends a change event if succesful (and a remove event if the new one * could not be added). */ private void fileChangedInCollection(FileDesc newFileDesc, FileDesc oldFileDesc, SharedFileCollection collection) { List<FileView> changedViews = null; List<FileView> removedViews = null; boolean addable = isFileAddable(newFileDesc); rwLock.writeLock().lock(); try { if(allSharedFilesView.fileRemovedFromView(oldFileDesc, collection, false)) { if(addable && allSharedFilesView.fileAddedFromView(newFileDesc, collection)) { changedViews = new ArrayList<FileView>(); changedViews.add(allSharedFilesView); } else { removedViews = new ArrayList<FileView>(); removedViews.add(allSharedFilesView); } } for(String id : collection.getFriendList()) { MultiFileView view = fileViewsPerFriend.get(id); if(view != null) { if(view.fileRemovedFromView(oldFileDesc, collection, false)) { if(addable && view.fileAddedFromView(newFileDesc, collection)) { if(changedViews == null) { changedViews = new ArrayList<FileView>(); } changedViews.add(view); LOG.debugf("File {0} changed from old file {1} in collection {1}, changing view {2}", newFileDesc, oldFileDesc, collection, view); } else if(!view.contains(newFileDesc)) { if(removedViews == null) { removedViews = new ArrayList<FileView>(); } removedViews.add(view); LOG.debugf("File {0} changed from old file {1} in collection {1}, couldn't add new file to view {2}", newFileDesc, oldFileDesc, collection, view); } } } } } finally { rwLock.writeLock().unlock(); } if(changedViews != null) { for(FileView view : changedViews) { multicaster.broadcast(new FileViewChangeEvent(view, Type.FILE_CHANGED, oldFileDesc, newFileDesc)); } } if(removedViews != null) { for(FileView view : removedViews) { multicaster.broadcast(new FileViewChangeEvent(view, Type.FILE_REMOVED, oldFileDesc)); } } } /** * Notification that metadata about a filedesc has changed in a shared collection. * * This could be because URNs added, or metadata changed. We have to take different * actions in different situations here. If a URN was added, we want to add the file * to the views. If metadata changed, we want to possibly remove the file from the view, * because it may be a store file (which shouldn't be in views!). If the file already existed * in the collection, we just want to change the metadata about it. */ private void fileMetaChangedInCollection(FileDesc fd, SharedFileCollection collection) { if(isFileAddable(fd)) { fileAddedToCollection(fd, collection, true); } else { fileRemovedFromCollection(fd, collection, true); } } /** * Notification that a file was added to a shared collection. * * If the collection was shared with any friends that have a view created, * and the file does not already exist in the view, the file will be added * to the view and an event will be sent for that view. * * If the file already existed in the collection, and change is true, * we send a notification that the meta changed for the file. */ private void fileAddedToCollection(FileDesc fileDesc, SharedFileCollection collection, boolean changed) { List<FileView> addedViews = null; List<FileView> changedViews = null; rwLock.writeLock().lock(); try { List<String> friendList = collection.getFriendList(); if(!friendList.isEmpty()) { // if the list was shared with anyone, add it to our all shared list! if(allSharedFilesView.fileAddedFromView(fileDesc, collection)) { addedViews = new ArrayList<FileView>(); addedViews.add(allSharedFilesView); } else if(changed) { // it was already in the view, so fire a changed event. changedViews = new ArrayList<FileView>(); changedViews.add(allSharedFilesView); } } for(String id : friendList) { MultiFileView view = fileViewsPerFriend.get(id); if(view != null) { if(view.fileAddedFromView(fileDesc, collection)) { if(addedViews == null) { addedViews = new ArrayList<FileView>(); } addedViews.add(view); LOG.debugf("File {0} added to collection {1}, changing view {2}", fileDesc, collection, view); } else if(changed) { LOG.debugf("File {0} changed in collection {1}, changing view {2}", fileDesc, collection, view); if(changedViews == null) { changedViews = new ArrayList<FileView>(); } changedViews.add(view); } else { if(LOG.isDebugEnabled()) { LOG.debugf("File {0} added to collection {1}, but didn't change view {2}, view contains file ? {3}", fileDesc, collection, view, view.contains(fileDesc)); } } } } } finally { rwLock.writeLock().unlock(); } if(addedViews != null) { for(FileView view : addedViews) { multicaster.broadcast(new FileViewChangeEvent(view, Type.FILE_ADDED, fileDesc)); } } if(changedViews != null) { for(FileView view : changedViews) { multicaster.broadcast(new FileViewChangeEvent(view, Type.FILE_META_CHANGED, fileDesc)); } } } @Override public FileView getFileViewForId(String id) { MultiFileView view; rwLock.readLock().lock(); try { view = fileViewsPerFriend.get(id); } finally { rwLock.readLock().unlock(); } if(view == null) { rwLock.writeLock().lock(); try { // recheck within lock -- it may have been created view = fileViewsPerFriend.get(id); if(view == null) { view = createFileView(id); fileViewsPerFriend.put(id, view); } } finally { rwLock.writeLock().unlock(); } } return view; } private MultiFileView createFileView(String id) { LOG.debugf("Creating new file view for id {0}", id); MultiFileView view = new MultiFileView(id); initialize(view, id); return view; } private void initialize(MultiFileView view, String id) { for(SharedFileCollection collection : sharedCollections) { if(collection.getFriendList().contains(id)) { LOG.debugf("Adding backing view of {0} to view for id {1}", collection, id); view.addNewBackingView(collection); } } } /** * Determines if the FileDesc can be added to a view. FileDescs without URNs * and files that are shareable. */ private boolean isFileAddable(FileDesc fd) { return fd.getSHA1Urn() != null && fd.isShareable(); } /** * An implementation of {@link FileView} that is backed by other FileViews. * This implementation is intended to work in concert with {@link FileViewManagerImpl} * and is used to return the view of files that any single person in a * {@link SharedFileCollection#getFriendList()}. Many different collections * can be shared with a single id. This {@link MultiFileView} represents a view * of everything that is shared with that id. */ private class MultiFileView extends AbstractFileView { /* * A note about locking: * All write locking is performed by FileViewManagerImpl. * This works because there are no public methods in this class * that are mutable. */ /** All views this is backed off of. */ private final List<FileView> backingViews = new ArrayList<FileView>(); private volatile long totalFileSize = 0; private final String name; MultiFileView(String name) { super(FileViewManagerImpl.this.library); this.name = name; } @Override public String getName() { return name; } @Override public String toString() { return StringUtils.toString(this); } @Override public long getNumBytes() { return totalFileSize; } @Override public void addListener(EventListener<FileViewChangeEvent> listener) { multicaster.addListener(this, listener); } @Override public Lock getReadLock() { return rwLock.readLock(); } @Override public Iterator<FileDesc> iterator() { rwLock.readLock().lock(); try { return new FileViewIterator(library, new IntSet(getInternalIndexes())); } finally { rwLock.readLock().unlock(); } } @Override public Iterable<FileDesc> pausableIterable() { return new Iterable<FileDesc>() { @Override public Iterator<FileDesc> iterator() { return MultiFileView.this.iterator(); } }; } @Override public boolean removeListener(EventListener<FileViewChangeEvent> listener) { return multicaster.removeListener(this, listener); } /** * Instructs the view to clear all its items. * This is typically because the library was cleared. * All collections are still backing collections. */ void clear() { getInternalIndexes().clear(); totalFileSize = 0; } /** * Removes a backing {@link FileView}. Not every FileDesc in the backing * view will necessarily be removed. This is because the FileDesc may exist * in another view that this is backed by. * * @return A list of {@link FileDesc}s that were removed from this view. */ List<FileDesc> removeBackingView(FileView view) { if (backingViews.remove(view)) { return validateItems(); } else { return Collections.emptyList(); } } /** * Adds a new backing {@link FileView}. Not every FileDesc in the backing * view will necessarily be added. This is because some FileDescs may * already exist due to other backing views. * * @return A list of {@link FileDesc}s that were added. */ List<FileDesc> addNewBackingView(FileView view) { if(!backingViews.contains(view)) { backingViews.add(view); // lock the backing view in order to iterate through its // indexes. List<FileDesc> added = new ArrayList<FileDesc>(view.size()); view.getReadLock().lock(); try { IntSetIterator iter = ((AbstractFileView)view).getInternalIndexes().iterator(); while(iter.hasNext()) { int i = iter.next(); FileDesc fd = library.getFileDescForIndex(i); if(isFileAddable(fd) && getInternalIndexes().add(i)) { added.add(fd); totalFileSize += fd.getFileSize(); } } } finally { view.getReadLock().unlock(); } return added; } else { // we already had this backing view -- no need to redo it. return Collections.emptyList(); } } /** * Notification that a backing {@link FileView} has been cleared. * * @return A list of {@link FileDesc}s that were removed from this view due * to being removed from the backing view. */ List<FileDesc> fileViewCleared(FileView fileView) { if(backingViews.contains(fileView)) { return validateItems(); } else { return Collections.emptyList(); } } /** * Notification that a {@link FileDesc} was removed from a backing {@link FileView}. * If force is true, this will remove the FileDesc even if other backing * views contained it. * * @return true if the file used to exist in this view (and is now removed). * false if it did not exist in this view or still exists. */ boolean fileRemovedFromView(FileDesc fileDesc, FileView fileView, boolean force) { if(!force) { for(FileView view : backingViews) { if(view.contains(fileDesc)) { return false; } } } getInternalIndexes().remove(fileDesc.getIndex()); totalFileSize -= fileDesc.getFileSize(); return true; } /** * Notification that a {@link FileDesc} was adding to a backing * {@link FileView}. * * @return true if the {@link FileDesc} was succesfully added to this view. * false if the FileDesc already existed in the view. */ boolean fileAddedFromView(FileDesc fileDesc, FileView fileView) { boolean added = getInternalIndexes().add(fileDesc.getIndex()); if(added) { totalFileSize += fileDesc.getFileSize(); } return added; } /** * Calculates what should be in this list based on the current * views in {@link #backingViews}. This will return a list * of {@link FileDesc} that is every removed item. * This method does not expect items to be added that were * not already contained. */ private List<FileDesc> validateItems() { // Calculate the current FDs in the set. IntSet newItems = new IntSet(); for(FileView view : backingViews) { view.getReadLock().lock(); try { newItems.addAll(((AbstractFileView)view).getInternalIndexes()); } finally { view.getReadLock().unlock(); } } library.filterIndexes(newItems, new Predicate<FileDesc>() { @Override public boolean apply(FileDesc t) { return isFileAddable(t); } }); // Calculate the FDs that were removed. List<FileDesc> removedFds; IntSet indexes = getInternalIndexes(); indexes.removeAll(newItems); if(indexes.size() == 0) { removedFds = Collections.emptyList(); } else { removedFds = new ArrayList<FileDesc>(indexes.size()); IntSetIterator iter = indexes.iterator(); while(iter.hasNext()) { FileDesc fd = library.getFileDescForIndex(iter.next()); if(fd != null) { removedFds.add(fd); totalFileSize -= fd.getFileSize(); } } } // Set the current FDs & return the removed ones. indexes.clear(); indexes.addAll(newItems); return removedFds; } } }