package edu.washington.cs.oneswarm.ui.gwt.server;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import org.bouncycastle.util.encoders.Base64;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.download.DownloadManagerState;
import org.gudy.azureus2.core3.global.GlobalManagerListener;
import org.gudy.azureus2.core3.torrent.TOTorrentException;
import org.gudy.azureus2.core3.torrent.TOTorrentFile;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.HashWrapper;
import com.aelitis.azureus.core.impl.AzureusCoreImpl;
import edu.washington.cs.oneswarm.MetaInfoPruner;
import edu.washington.cs.oneswarm.f2f.FileCollection;
import edu.washington.cs.oneswarm.f2f.FileList;
import edu.washington.cs.oneswarm.f2f.Friend;
import edu.washington.cs.oneswarm.f2f.FriendConnectListener;
import edu.washington.cs.oneswarm.f2f.multisource.Sha1DownloadManager;
import edu.washington.cs.oneswarm.ui.gwt.CoreInterface;
import edu.washington.cs.oneswarm.ui.gwt.CoreTools;
import edu.washington.cs.oneswarm.ui.gwt.client.newui.FileTypeFilter;
import edu.washington.cs.oneswarm.ui.gwt.rpc.FileTree;
/**
* Returning a FileTree is a bit of a hack, but since we already have a
* serialized type that encapsulates tree, just reuse it here instead of cooking
* up a new one.
*/
class TreeScratch {
public TreeScratch(String k) {
tagName = k;
}
public List<TreeScratch> kids = new LinkedList<TreeScratch>();
public String tagName;
};
public class StatelessSwarmFilter {
private static Logger logger = Logger.getLogger(StatelessSwarmFilter.class.getName());
boolean shouldUpdateClient = false;
CoreInterface mCore = null;
enum SortMetric {
Name(new Comparator<DownloadManager>() {
public int compare(DownloadManager lhs, DownloadManager rhs) {
return (new String(lhs.getTorrent().getName()).toLowerCase().compareTo((new String(
rhs.getTorrent().getName())).toLowerCase()));
}
}), Date(new Comparator<DownloadManager>() {
public int compare(DownloadManager lhs, DownloadManager rhs) {
/**
* Special case sorting one of our download managers when
* viewing a friend's files only
*/
long lhs_time = lhs.getData("friend-added-time") != null ? (Long) lhs
.getData("friend-added-time") : lhs.getDownloadState().getLongParameter(
DownloadManagerState.PARAM_DOWNLOAD_ADDED_TIME);
long rhs_time = rhs.getData("friend-added-time") != null ? (Long) rhs
.getData("friend-added-time") : rhs.getDownloadState().getLongParameter(
DownloadManagerState.PARAM_DOWNLOAD_ADDED_TIME);
long foo = -(lhs_time - rhs_time);
if (foo < 0)
return -1;
else if (foo > 0)
return 1;
else
return 0;
}
}), Size(new Comparator<DownloadManager>() {
public int compare(DownloadManager lhs, DownloadManager rhs) {
long foo = (lhs.getTorrent().getSize() - rhs.getTorrent().getSize());
// we flip the order here to have largest first
if (foo < 0)
return 1;
else if (foo > 0)
return -1;
else
return 0;
}
});
Comparator<DownloadManager> mComp;
SortMetric(Comparator<DownloadManager> comp) {
mComp = comp;
}
};
public StatelessSwarmFilter(CoreInterface inCore) {
mCore = inCore;
AzureusCoreImpl.getSingleton().getGlobalManager().addListener(new GlobalManagerListener() {
public void destroyInitiated() {
}
public void destroyed() {
}
public void downloadManagerAdded(DownloadManager dm) {
logger.finer("dl manager added forces client refresh: " + dm.getDisplayName());
shouldUpdateClient = true;
}
public void downloadManagerRemoved(DownloadManager dm) {
logger.finer("dl manager removed forces client refresh: " + dm.getDisplayName());
shouldUpdateClient = true;
String base64Hash = null;
try {
base64Hash = new String(Base64.encode(dm.getTorrent().getHash()));
filteredUntilDeleteBase64.remove(base64Hash);
} catch (TOTorrentException e) {
e.printStackTrace();
}
}
public void seedingStatusChanged(boolean seeding_only_mode) {
}
});
mCore.getF2FInterface().registerForFriendConnectNotifications(new FriendConnectListener() {
public void friendConnected(Friend f) {
logger.finer("friend connected forces client refresh: " + f.getNick());
shouldUpdateClient = true;
/**
* Record our observing these so we don't prune them
*/
FileList flist = mCore.getF2FInterface().getOnlineFileLists().get(f);
for (FileCollection coll : flist.getElements()) {
String hexHash = ByteFormatter.encodeString(Base64.decode(coll.getUniqueID()));
MetaInfoPruner.get().recordActiveHash(hexHash);
}
}
public void friendDisconnected(Friend f) {
logger.finer("friend disconnected forces client refresh: " + f.getNick());
shouldUpdateClient = true;
}
});
}
public boolean shouldClientSideUIRefresh() {
return shouldUpdateClient;
}
public void willUpdate() {
shouldUpdateClient = false;
}
private List<DownloadManager> getSingleFriendsFiles(int selectedFriendID) {
List<DownloadManager> out = new ArrayList<DownloadManager>();
Friend friend = null;
for (Friend candidate : mCore.getF2FInterface().getOnlineFileLists().keySet()) {
if (candidate.getConnectionId() == selectedFriendID) {
friend = candidate;
break;
}
}
if (friend == null) {
logger.warning("couldn't find supposedly online friend! id: " + selectedFriendID);
return out;
}
FileList fileList = mCore.getF2FInterface().getOnlineFileLists().get(friend);
System.out.println("single friend's flist: " + friend.getNick() + " / " + fileList);
if (fileList == null) {
logger.warning("null file list for friend: " + friend.getNick());
return out;
}
for (FileCollection collection : fileList.getElements()) {
/**
* If we have this file ourselves, no need to show a 'download'
* option, we'll just include the local copy
*/
DownloadManager ours = AzureusCoreImpl.getSingleton().getGlobalManager()
.getDownloadManager(new HashWrapper(Base64.decode(collection.getUniqueID())));
if (ours != null) {
/**
* This allows fast deletes (regardless of how long Azureus
* takes to remove things on the back end)
*/
String base64Hash = null;
try {
base64Hash = new String(Base64.encode(ours.getTorrent().getHash()));
} catch (TOTorrentException e) {
e.printStackTrace();
continue;
}
if (filteredUntilDeleteBase64.contains(base64Hash)) {
DownloadManagerAdapter adapter = new DownloadManagerAdapter(collection,
friend.getConnectionId(), friend.getNick());
out.add(adapter);
} else {
out.add(ours);
ours.setData("friend-added-time", new Long(collection.getAddedTimeUTC()));
}
} else {
DownloadManagerAdapter adapter = new DownloadManagerAdapter(collection,
friend.getConnectionId(), friend.getNick());
out.add(adapter);
}
}
return out;
}
enum SpecialMatcher {
NOT1("not:") {
boolean match(DownloadManager dm, String text) {
return !deepKeywordMatch(dm, text);
}
},
NOT2("-") {
boolean match(DownloadManager dm, String text) {
return NOT1.match(dm, text);
}
};
String mLabel;
SpecialMatcher(String inLabel) {
mLabel = inLabel;
}
abstract boolean match(DownloadManager candidate, String text);
};
private static boolean deepKeywordMatch(DownloadManager d, String keyword) {
/**
* If the display name or any interior file...
*/
if (d.getDisplayName().toLowerCase().contains(keyword) == false) {
/**
* Metainfo -- artist/album attributes if they exist
*/
String album = d.getDownloadState().getAttribute(
FileCollection.ONESWARM_ALBUM_ATTRIBUTE);
if (album != null) {
if (album.toLowerCase().contains(keyword)) {
return true;
}
}
String artist = d.getDownloadState().getAttribute(
FileCollection.ONESWARM_ARTIST_ATTRIBUTE);
if (artist != null) {
if (artist.toLowerCase().contains(keyword)) {
return true;
}
}
/**
* 2) filenames
*/
for (TOTorrentFile f : d.getTorrent().getFiles()) {
if (f.getRelativePath().toLowerCase().contains(keyword)) {
return true;
}
}
return false;
}
/**
* When here, display name matches.
*/
return true;
}
long keyword_time = 0;
private boolean matchKeywords(DownloadManager d, String[] inKeywords) {
long start = System.currentTimeMillis();
if (inKeywords == null)
return true;
if (inKeywords.length == 0)
return true;
boolean matchedSpecial;
for (String keyword : inKeywords) {
matchedSpecial = false;
for (SpecialMatcher sm : SpecialMatcher.values()) {
if (keyword.toLowerCase().startsWith(sm.mLabel)) {
if (sm.match(d, keyword.toLowerCase().substring(sm.mLabel.length()))) {
matchedSpecial = true;
break;
}
}
}
if (matchedSpecial == false) {
if (deepKeywordMatch(d, keyword.toLowerCase()) == false) {
keyword_time += System.currentTimeMillis() - start;
return false;
}
}
}
keyword_time += System.currentTimeMillis() - start;
return true;
}
// public static String getLargestFileName(DownloadManager dm) {
// if (dm.getTorrent() != null) {
// TOTorrentFile[] files_orig = dm.getTorrent().getFiles();
//
// TOTorrentFile largest = files_orig[0];
// for (TOTorrentFile f : files_orig) {
// if (f.getLength() > largest.getLength()) {
// largest = f;
// }
// }
//
// return largest.getRelativePath();
// } else {
// System.err.println("null torrent!: " + dm.getDisplayName());
// return "";
// }
// }
private boolean matchType(DownloadManager d, FileTypeFilter inFileType) {
final TOTorrentFile biggestFile = CoreTools.getBiggestFile(d, false);
String largestFileName = "";
if (biggestFile != null) {
largestFileName = biggestFile.getRelativePath();
}
if (inFileType.equals(FileTypeFilter.All)) {
return true;
} else if (FileTypeFilter.match(largestFileName).equals(inFileType)) {
return true;
}
return false;
}
/**
* This allows fast deletes (regardless of how long Azureus takes to remove
* things on the back end)
*/
Set<String> filteredUntilDeleteBase64 = new HashSet<String>();
public void filterUntilDelete(byte[] inHash) {
String toAdd = new String(Base64.encode(inHash));
filteredUntilDeleteBase64.add(toAdd);
shouldUpdateClient = true;
System.out.println("filterUntilDelete, trying force update " + toAdd);
}
public class FilteredSwarmInfo {
public List<DownloadManager> outSwarms = null;
public int total_swarms_in_type = 0;
public FileTree tags = null;
public boolean truncated_tags = false;
};
@SuppressWarnings("unchecked")
public FilteredSwarmInfo filterSwarms(String[] inKeywords, SortMetric inSortingMetric,
FileTypeFilter inFileType, boolean includeF2F, int selectedFriendID, String inTagPath) {
keyword_time = 0;
long start = System.currentTimeMillis();
Set<String> stillFilteredUntilDelete = new HashSet<String>();
FilteredSwarmInfo outInfo = new FilteredSwarmInfo();
outInfo.outSwarms = new ArrayList<DownloadManager>();
/**
* We start by generating the list of all swarm and then sorting
* according to the metric
*/
logger.finer("filtered contains: " + filteredUntilDeleteBase64.size());
List<DownloadManager> filteredExceptByTag = new LinkedList<DownloadManager>();
if (selectedFriendID != Integer.MIN_VALUE) {
/**
* If this isn't MIN_VALUE, we're looking at a single friend's files
* only.
*/
List<DownloadManager> singleFriendsFiles = getSingleFriendsFiles(selectedFriendID);
for (DownloadManager d : singleFriendsFiles) {
if (matchKeywords(d, inKeywords) == false) {
continue;
}
filteredExceptByTag.add(d);
if (matchTags(d, inTagPath) == false) {
continue;
}
outInfo.outSwarms.add(d);
}
outInfo.tags = getTagsFromSwarms(filteredExceptByTag);
outInfo.total_swarms_in_type = outInfo.outSwarms.size();
} else {
Set<String> ourHashes = new HashSet<String>();
/**
* First, add all our files
*/
for (DownloadManager d : (List<DownloadManager>) AzureusCoreImpl.getSingleton()
.getGlobalManager().getDownloadManagers()) {
if (d.getTorrent() == null) {
logger.warning("null torrent!: " + d.getDisplayName());
continue;
}
if (d.getDownloadState().getBooleanAttribute(
Sha1DownloadManager.ONESWARM_AUTO_ADDED)) {
continue;
}
// clear this if it exists to avoid sorting problems in the main
// view.
d.setData("friend-added-time", null);
if (matchType(d, inFileType) == false) {
continue;
}
String base64Hash = null;
try {
base64Hash = new String(Base64.encode(d.getTorrent().getHash()));
} catch (TOTorrentException e) {
e.printStackTrace();
continue;
}
/**
* This allows fast deletes (regardless of how long Azureus
* takes to remove things on the back end)
*/
if (filteredUntilDeleteBase64.contains(base64Hash)) {
stillFilteredUntilDelete.add(base64Hash);
continue;
}
outInfo.total_swarms_in_type++;
if (matchKeywords(d, inKeywords) == false) {
continue;
}
/**
* We need to maintain this list to preserve the list of tags
* (which we also generate here) even if a tag filter has been
* set.
*/
filteredExceptByTag.add(d);
if (matchTags(d, inTagPath) == false) {
continue;
}
outInfo.outSwarms.add(d);
ourHashes.add(base64Hash);
}
filteredUntilDeleteBase64 = stillFilteredUntilDelete;
/**
* Next, if selected, friend's files
*/
if (includeF2F) {
Set<String> friendHashes = new HashSet<String>();
for (Friend friend : mCore.getF2FInterface().getOnlineFileLists().keySet()) {
FileList fileList = mCore.getF2FInterface().getOnlineFileLists().get(friend);
if (fileList == null) {
logger.warning("null filelist for friend!!: " + friend.getNick());
continue;
}
for (FileCollection collection : fileList.getElements()) {
/**
* Skip files that multiple friends have. We'll discover
* these if needed during download. Also skip files that
* we have locally
*/
if (friendHashes.contains(collection.getUniqueID())
|| ourHashes.contains(collection.getUniqueID())) {
continue;
}
DownloadManagerAdapter adapter = new DownloadManagerAdapter(collection,
friend.getConnectionId(), friend.getNick());
if (matchType(adapter, inFileType) == false)
continue;
outInfo.total_swarms_in_type++;
if (matchKeywords(adapter, inKeywords) == false)
continue;
filteredExceptByTag.add(adapter);
if (matchTags(adapter, inTagPath) == false) {
continue;
}
outInfo.outSwarms.add(adapter);
friendHashes.add(collection.getUniqueID());
}
}
}
}
outInfo.tags = getTagsFromSwarms(filteredExceptByTag);
int maxTags = COConfigurationManager.getIntParameter("oneswarm.max.ui.tags");
if (maxTags != 0) {
MutableInt count = new MutableInt();
count.v = 0;
outInfo.truncated_tags = pruneTagsBasedOnLimit(outInfo.tags, count, maxTags);
}
/**
* Finally, sort
*/
Collections.sort(outInfo.outSwarms, inSortingMetric.mComp);
logger.fine("keyword time: " + keyword_time + " total: "
+ (System.currentTimeMillis() - start));
return outInfo;
}
static class MutableInt {
public int v;
}
private boolean pruneTagsBasedOnLimit(FileTree tags, MutableInt count, int max) {
/**
* Visit this node.
*/
count.v++;
/**
* Visit children in order
*/
int removed = 0;
for (int i = 0; i < tags.getChildren().length; i++) {
if (count.v > max) {
tags.getChildren()[i] = null;
removed++;
} else {
count.v++;
}
}
/**
* Compact if we removed anything
*/
if (removed > 0) {
FileTree[] neu = new FileTree[tags.getChildren().length - removed];
int where = 0;
for (FileTree old : tags.getChildren()) {
if (old != null) {
neu[where++] = old;
}
}
tags.setChildren(neu);
}
boolean truncatedKid = false;
for (int i = 0; i < tags.getChildren().length; i++) {
truncatedKid = truncatedKid || pruneTagsBasedOnLimit(tags.getChildren()[i], count, max);
}
return removed > 0 || truncatedKid;
}
private boolean matchTags(DownloadManager d, String inTagPath) {
if (inTagPath == null) {
return true;
}
if (inTagPath.length() == 0) {
return true;
}
String[] tags = d.getDownloadState().getListAttribute(
FileCollection.ONESWARM_TAGS_ATTRIBUTE);
if (tags == null) {
return false;
}
for (String tag : tags) {
if (tag.startsWith(inTagPath)) {
return true;
}
}
return false;
}
public static FileTree getTagsFromSwarms(List<DownloadManager> outSwarms) {
try {
TreeScratch root = new TreeScratch("");
if (COConfigurationManager.getBooleanParameter("oneswarm.show.tags") == false) {
return convertScratchToFileTree(root); // no tags
}
for (org.gudy.azureus2.core3.download.DownloadManager dm : outSwarms) {
String[] tags_raw = dm.getDownloadState().getListAttribute(
FileCollection.ONESWARM_TAGS_ATTRIBUTE);
if (tags_raw == null) {
continue;
}
// convert this into a set of paths
List<List<String>> tags = new LinkedList<List<String>>();
for (String tag : tags_raw) {
LinkedList<String> ll = new LinkedList<String>();
for (String s : tag.split("/")) {
ll.add(s);
}
tags.add(ll);
}
for (List<String> path : tags) {
findTreeMatch(root, path);
}
}
// now convert the TreeScratch into a FileTree to deliver to client
return convertScratchToFileTree(root);
} catch (Exception e) {
System.err.println(e.toString());
}
return null;
}
private static FileTree convertScratchToFileTree(TreeScratch node) {
FileTree ftree = new FileTree();
ftree.setName(node.tagName);
if (node.kids.size() == 0) {
return ftree;
} else {
FileTree[] convertedKids = new FileTree[node.kids.size()];
int i = 0;
for (TreeScratch k : node.kids) {
convertedKids[i++] = convertScratchToFileTree(k);
}
Arrays.sort(convertedKids, new Comparator<FileTree>() {
public int compare(FileTree o1, FileTree o2) {
return o1.getName().compareTo(o2.getName());
}
});
ftree.setChildren(convertedKids);
return ftree;
}
}
private static void findTreeMatch(TreeScratch node, List<String> remainingMatch) {
if (remainingMatch.size() == 0) {
// duplicate
return;
}
for (TreeScratch kid : node.kids) {
if (remainingMatch.get(0).equals(kid.tagName)) {
remainingMatch.remove(0);
findTreeMatch(kid, remainingMatch);
return;
}
}
// need to add the rest of these as kids in the unified tree
TreeScratch curr = node;
for (String tag : remainingMatch) {
TreeScratch neu = new TreeScratch(tag);
curr.kids.add(neu);
curr = neu;
}
}
}