package org.limewire.core.impl.library;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.jcip.annotations.GuardedBy;
import org.limewire.collection.glazedlists.AbstractListEventListener;
import org.limewire.core.api.browse.Browse;
import org.limewire.core.api.browse.BrowseFactory;
import org.limewire.core.api.browse.BrowseListener;
import org.limewire.core.api.library.FriendLibrary;
import org.limewire.core.api.library.PresenceLibrary;
import org.limewire.core.api.library.RemoteLibraryManager;
import org.limewire.core.api.library.RemoteLibraryState;
import org.limewire.core.api.search.SearchResult;
import org.limewire.core.impl.friend.FriendRemoteFileDescDeserializer;
import org.limewire.core.impl.search.RemoteFileDescAdapter;
import org.limewire.friend.api.FriendPresence;
import org.limewire.friend.api.LibraryChangedEvent;
import org.limewire.friend.api.feature.AddressFeature;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.Address;
import org.limewire.listener.EventListener;
import org.limewire.listener.ListenerSupport;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.net.ConnectivityChangeEvent;
import org.limewire.net.SocketsManager;
import org.limewire.net.address.AddressResolutionObserver;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import com.google.inject.Inject;
@EagerSingleton
class PresenceLibraryBrowser implements EventListener<LibraryChangedEvent> {
private static final Log LOG = LogFactory.getLog(PresenceLibraryBrowser.class);
private final BrowseFactory browseFactory;
private final RemoteLibraryManager remoteLibraryManager;
private final SocketsManager socketsManager;
/**
* Keeps track of libraries that could not be browsed yet, because the local peer didn't have
* enough connection capabilities.
*/
final Set<PresenceLibrary> librariesToBrowse = Collections.synchronizedSet(new HashSet<PresenceLibrary>());
/**
* Is incremented when a new connectivity change event is received, should
* only be modified holding the lock to {@link #librariesToBrowse}.
*
* When address resolution fails, the revision that was used when resolution is started
* can be compared to the latest revision to see if the client's connectivity
* has changed in the meantime and resolution should be retried.
*
* Still volatile, so it can be read without a lock.
*/
@GuardedBy("librariesToBrowse")
private volatile int latestConnectivityEventRevision = 0;
@Inject
public PresenceLibraryBrowser(BrowseFactory browseFactory, RemoteLibraryManager remoteLibraryManager,
SocketsManager socketsManager, FriendRemoteFileDescDeserializer remoteFileDescDeserializer) {
this.browseFactory = browseFactory;
this.remoteLibraryManager = remoteLibraryManager;
this.socketsManager = socketsManager;
}
@Inject
void register(ListenerSupport<LibraryChangedEvent> listenerSupport) {
listenerSupport.addListener(this);
}
@Inject
void registerToSocksManager() {
socketsManager.addListener(new ConnectivityChangeListener());
}
@Inject
void registerToRemoteLibraryManager() {
remoteLibraryManager.getFriendLibraryList().addListEventListener(new ListEventListener<FriendLibrary>() {
@Override
public void listChanged(ListEvent<FriendLibrary> listChanges) {
while(listChanges.next()) {
if(listChanges.getType() == ListEvent.INSERT) {
final FriendLibrary friendLibrary = listChanges.getSourceList().get(listChanges.getIndex());
new AbstractListEventListener<PresenceLibrary>() {
@Override
protected void itemAdded(PresenceLibrary presenceLibrary, int idx, EventList<PresenceLibrary> source) {
tryToResolveAndBrowse(presenceLibrary, latestConnectivityEventRevision);
}
@Override
protected void itemRemoved(PresenceLibrary item, int idx, EventList<PresenceLibrary> source) {
// TODO: This should cancel the browse, if it was in action.
librariesToBrowse.remove(item);
}
@Override
protected void itemUpdated(PresenceLibrary item, PresenceLibrary prior, int idx, EventList<PresenceLibrary> source) {
}
}.install(friendLibrary.getPresenceLibraryList());
}
}
}
});
}
@Override
public void handleEvent(LibraryChangedEvent event) {
// The idea behind this is that we want to provide incremental updates to
// a PresenceLibrary, without requiring the entire library disappear
// and reappear. We need to know if adding the presence library succeeded,
// but also need to trigger a browse if it didn't (because it already existed).
FriendPresence friend = event.getData();
PresenceLibrary existingLibrary = remoteLibraryManager.getPresenceLibrary(friend);
if(!remoteLibraryManager.addPresenceLibrary(friend) && existingLibrary != null) {
LOG.debugf("Library changed event for {0}, but existing library -- rebrowsing into existing library", friend);
// the library already existed for this presence --
// we need to trigger our own browse.
// There's a small chance the existingLibrary is an older version of
// a PresenceLibrary (not the current one) -- if that does happen,
// the worst this will do is cause a second browse to happen.
tryToResolveAndBrowse(existingLibrary, latestConnectivityEventRevision);
}
}
void browse(final PresenceLibrary presenceLibrary) {
// TODO: Is this needed again? We should already be in loading
presenceLibrary.setState(RemoteLibraryState.LOADING);
final FriendPresence friendPresence = presenceLibrary.getPresence();
AddressFeature addressFeature = ((AddressFeature)friendPresence.getFeature(AddressFeature.ID));
if(addressFeature == null) {
// happens during sign-off
return;
}
LOG.debugf("browsing {0} ...", friendPresence.getPresenceId());
final Browse browse = browseFactory.createBrowse(friendPresence);
// TODO: We need to capture the Browse and call stop on it when the library is removed,
// otherwise the browse can be lingering in the background.
browse.start(new BrowseListener() {
// Build an in-transit list and replace at the end, but only if there's
// no existing list, or if we have enough memory to duplicate the list.
private final List<SearchResult> transitList;
// (anonymous constructor)
{
int size = presenceLibrary.size();
if(size == 0) {
transitList = null;
} else if (remoteLibraryManager.getAllFriendsLibrary().size() > 5000) {
// can run low on memory, so clear old list & add as new ones come.
presenceLibrary.clear();
transitList = null;
} else {
transitList = new ArrayList<SearchResult>(size);
}
}
public void handleBrowseResult(SearchResult searchResult) {
LOG.debugf("browse result: {0}, {1}", searchResult.getUrn(), searchResult.getSize());
RemoteFileDescAdapter remoteFileDescAdapter = (RemoteFileDescAdapter)searchResult;
// need to upgrade the RFD to be use the friendpresence.
remoteFileDescAdapter = new RemoteFileDescAdapter(remoteFileDescAdapter, friendPresence);
if(transitList != null) {
transitList.add(remoteFileDescAdapter);
} else {
presenceLibrary.addNewResult(remoteFileDescAdapter);
}
}
@Override
public void browseFinished(boolean success) {
if(transitList != null) {
LOG.debugf("Finished browse of {0}, setting resulting files into existing list", friendPresence);
presenceLibrary.setNewResults(transitList);
} else {
LOG.debugf("Finished browse of {0}, no in-transit list.", friendPresence);
}
if(success) {
presenceLibrary.setState(RemoteLibraryState.LOADED);
} else {
presenceLibrary.setState(RemoteLibraryState.FAILED_TO_LOAD);
LOG.debugf("browse failed: {0}", presenceLibrary);
}
}
});
}
/**
* Tries to resolve the address of <code>presenceLibrary<code> and browse it
* after successful resolution and/or if it can connect to the address. Otherwise,
* it handle the failure by calling {@link #handleFailedResolution(PresenceLibrary, int)}.
*
* @param presenceLibrary the presence library whose address should be resolved
* and browsed
* @param startConnectivityRevision the revisions of {@link #latestConnectivityEventRevision}
* when this method is called
*/
void tryToResolveAndBrowse(final PresenceLibrary presenceLibrary, final int startConnectivityRevision) {
presenceLibrary.setState(RemoteLibraryState.LOADING);
final FriendPresence friendPresence = presenceLibrary.getPresence();
AddressFeature addressFeature = (AddressFeature)friendPresence.getFeature(AddressFeature.ID);
if (addressFeature == null) {
LOG.debug("no address feature");
handleFailedResolution(presenceLibrary, startConnectivityRevision);
return;
}
Address address = addressFeature.getFeature();
if (socketsManager.canResolve(address)) {
socketsManager.resolve(address, new AddressResolutionObserver() {
@Override
public void resolved(Address address) {
if (socketsManager.canConnect(address)) {
LOG.debugf("resolved {0} for {1} and can connect", address, friendPresence);
browse(presenceLibrary);
} else {
LOG.debugf("resolved {0} for {1} and cannot connect", address, friendPresence);
handleFailedResolution(presenceLibrary, startConnectivityRevision);
}
}
@Override
public void handleIOException(IOException iox) {
LOG.debug("resolve error", iox);
handleFailedResolution(presenceLibrary, startConnectivityRevision);
}
@Override
public void shutdown() {
}
});
} else if (socketsManager.canConnect(address)) {
browse(presenceLibrary);
} else {
handleFailedResolution(presenceLibrary, startConnectivityRevision);
}
}
/**
* Called when resolution failed.
*
* If {@link #latestConnectivityEventRevision} is greater then <code>startRevision</code>,
* a new attempt at resolving the presence address is started, otherwise <code>presenceLibrary</code>
* is queued up in libraries to browse.
*
* @param presenceLibrary the library that could not be browsed
* @param startRevision the revision under which the address resolution attempt
* was started
*/
private void handleFailedResolution(PresenceLibrary presenceLibrary, int startRevision) {
LOG.debugf("failed resolution for:{0} revision:{1}", presenceLibrary.getPresence().getPresenceId(), startRevision);
presenceLibrary.setState(RemoteLibraryState.FAILED_TO_LOAD);
boolean retry;
synchronized (librariesToBrowse) {
retry = latestConnectivityEventRevision > startRevision;
if (!retry) {
LOG.debugf("readding and not trying after fail {0}", presenceLibrary);
boolean wasAdded = librariesToBrowse.add(presenceLibrary);
assert(wasAdded);
} else {
// copy value under lock
startRevision = latestConnectivityEventRevision;
LOG.debugf("retrying with new revision {0}", startRevision);
}
LOG.debugf("libraries to browser after fail: {0}, size {1}", librariesToBrowse, librariesToBrowse.size());
}
if (retry) {
tryToResolveAndBrowse(presenceLibrary, startRevision);
}
}
/**
* Is notified of better connection capabilities and iterates over the list of unbrowsable
* presence libraries to see if they can be browsed now.
*/
private class ConnectivityChangeListener implements EventListener<ConnectivityChangeEvent> {
/**
* Increments the {@link PresenceLibraryBrowser#latestConnectivityEventRevision}
* copies and empties {@link PresenceLibraryBrowser#librariesToBrowse} and
* tries calls {@link PresenceLibraryBrowser#tryToResolveAndBrowse(PresenceLibrary, int)}
* for each with the new revision.
*/
@Override
public void handleEvent(ConnectivityChangeEvent event) {
LOG.debug("connectivity change");
List<PresenceLibrary> copy;
int currentRevision;
synchronized (librariesToBrowse) {
currentRevision = ++latestConnectivityEventRevision;
copy = new ArrayList<PresenceLibrary>(librariesToBrowse);
librariesToBrowse.clear();
}
// outside of synchronized to avoid dead lock
LOG.debugf("revision: {0}, libraries to browse again: {1}", currentRevision, copy);
for (PresenceLibrary library : copy) {
tryToResolveAndBrowse(library, currentRevision);
}
}
}
}