package edu.washington.cs.oneswarm.f2f;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.bouncycastle.util.encoders.Base64;
import org.gudy.azureus2.core3.torrent.impl.TOTorrentImpl;
/**
* Reducing the memory footprint of this class would be great.
*/
public class FileCollection
{
private Logger logger = Logger.getLogger(FileCollection.class.getName());
private Map<String, String> optionalFields = new HashMap<String, String>();
public static final byte TYPE_BITTORRENT = 0;
private byte type;
private transient byte[] uniqueIDBytes;
private String uniqueID = "";
private String name = "";
private String description = "";
private String category = "";
private int hash = -1;
private List<FileListFile> children;
private List<List<String>> directoryTags = new LinkedList<List<String>>();
private long addedTimeUTC;
public static final String ONESWARM_ARTIST_ATTRIBUTE = TOTorrentImpl.OS_ARTIST;
public static final String ONESWARM_ALBUM_ATTRIBUTE = TOTorrentImpl.OS_ALBUM;
public static final String ONESWARM_TAGS_ATTRIBUTE = TOTorrentImpl.OS_TAGS;
public static final String ONESWARM_STREAM_ATTRIBUTE = TOTorrentImpl.OS_NO_STREAM;
public long getAddedTimeUTC() {
return addedTimeUTC;
}
public void setAddedTimeUTC(long addedTimeUTC) {
this.addedTimeUTC = addedTimeUTC;
}
private void setOptionalFields(Map<String, String> optionalFields) {
this.optionalFields = optionalFields;
}
public FileCollection(byte type, String uniqueID, String name,
String description, String category, List<FileListFile> children,
long addedTimeUTC) {
this.type = type;
this.uniqueID = uniqueID;
this.uniqueIDBytes = Base64.decode(uniqueID);
this.name = name;
this.description = description;
this.category = category;
this.children = children;
this.addedTimeUTC = addedTimeUTC;
calcHash();
// System.out.println("Created listelement: " + toString());
}
public String getCategory() {
return category;
}
public String getDescription() {
return description;
}
public void calcHash() {
hash = Arrays.hashCode(uniqueIDBytes) + directoryTags.hashCode();
}
public String getName() {
return name;
}
public byte getType() {
return type;
}
public String getUniqueID() {
return uniqueID;
}
public byte[] getUniqueIdBytes() {
return uniqueIDBytes;
}
public int hashCode() {
return hash;
}
public void setCategory(String category) {
this.category = category;
}
public void setDescription(String decription) {
this.description = decription;
}
public void setHash(int hash) {
this.hash = hash;
}
public void setName(String name) {
this.name = name;
}
public void setType(byte type) {
this.type = type;
}
public void setUniqueID(String uniqueID) {
this.uniqueID = uniqueID;
this.uniqueIDBytes = Base64.decode(uniqueID);
}
public String toString() {
return "t=" + type + " u=" + uniqueID + " n=" + name + " c=" + category
+ " d=" + description + " h=" + hashCode();
}
public int getHash() {
return hash;
}
public List<FileListFile> getChildren() {
return children;
}
public void setChildren(List<FileListFile> children) {
this.children = children;
}
public FileCollection clone() {
FileCollection fileCollection = new FileCollection(type, uniqueID, name,
description, category, new ArrayList<FileListFile>(children),
addedTimeUTC);
fileCollection.setOptionalFields(optionalFields);
return fileCollection;
}
public void setOptionalField(String key, String value) {
optionalFields.put(key, value);
}
/**
* Method for handling search, current policy
* If the id:xxx keyword is specified the collection must have the specified IDm otherwise null is returned
* If the search string matches the collection name, all files are returned
* Otherwise a subcollection containing only the files with matching names are returned
* If no files match, null is returned
* @param searchString
* @return
*/
public FileCollection searchMatches(String searchString) {
searchString = removeWhiteSpaceAfteKeyChars(searchString);
// handle the id: keyword, if id:xxx is specified, only collections with that id are valid
/*
* filter out thing not matching the keywords
*/
FileCollection keyWordHits = checkKeyWordMatch(searchString);
// null = not match on id
if (keyWordHits == null) {
return null;
}
// switch to lower case from now on
boolean metaInfoMatch = keyWordHits.nameMatch(searchString);
// if we have a match in the torrent name, return all files in torrent
if (metaInfoMatch) {
return keyWordHits.clone();
}
// else, return a new collection that only contains the files that matched
return keyWordHits.fileMatches(searchString);
}
/**
* Method for getting a subcollection that only contains the files that matches the search string
* @param searchString
* @return
*/
public FileCollection fileMatches(String searchString) {
List<FileListFile> searchMatches = new ArrayList<FileListFile>();
for (FileListFile file : children) {
if (file.searchMatch(searchString)) {
searchMatches.add(file);
}
}
if (searchMatches.size() > 0) {
FileCollection fileCollection = new FileCollection(type, uniqueID, name,
description, category, searchMatches, addedTimeUTC);
fileCollection.setOptionalFields(optionalFields);
return fileCollection;
} else {
return null;
}
}
/**
* Method for determining if the name of the collection matches the search string
* @param searchString space separated words
* @return true if all keywords are in the name, false otherwise
*/
public boolean nameMatch(String searchString) {
searchString = searchString.toLowerCase();
String n = name.toLowerCase();
String[] searchTerms = quoteRespectingSplit(searchString);
for (String term : searchTerms) {
// ignore terms that contain ':'
if (!containsKeyword(term)) {
boolean negatedTerm = term.startsWith("-");
if (negatedTerm) {
term = term.substring(1);
System.out.println("got negated term:" + term);
}
// check if the name contains the term
if (negatedTerm == n.contains(term)) {
// ok, last shot, does the term exist in any of the optional fields?
if (negatedTerm == optionalFieldHit(term)) {
// no match, this term does not match either name nor any optional field
return false;
}
}
}
}
return true;
}
/**
* returns true if one of the optional fields (like artist or album) contains the term
* @param searchString
* @return
*/
public boolean optionalFieldHit(String term) {
for (String p : optionalFields.values()) {
if (p.toLowerCase().contains(term)) {
return true;
}
}
return false;
}
/**
* Method for getting the largest file in the file collection that has a name that matches the search string
* @param searchString space separated words
* @return a new collection containing only the largest file that matches
*/
public FileCollection getLargestFile(String searchString) {
searchString = searchString.toLowerCase();
FileCollection matchingFiles = fileMatches(searchString);
if (matchingFiles == null) {
return null;
} else {
FileCollection largestMatchingFile = matchingFiles.getLargestFile();
return largestMatchingFile;
}
}
/**
* Method for getting the largest file in the collection
* @return a new collection containing only the largest file
*/
public FileCollection getLargestFile() {
FileListFile largestFile = null;
long largestSize = 0;
for (FileListFile f : children) {
if (f.getLength() > largestSize) {
largestSize = f.getLength();
largestFile = f;
}
}
List<FileListFile> l = new ArrayList<FileListFile>();
l.add(largestFile);
FileCollection fileCollection = new FileCollection(type, uniqueID, name,
description, category, l, addedTimeUTC);
fileCollection.setOptionalFields(optionalFields);
return fileCollection;
}
// versions less than 0.67 will respond with everything for : searches (bug...), allow both : and ; for now
public static String[] KEYWORDENDINGS = new String[] {
":",
";"
};
/**
* Filters a file connections based on keywords
* @param searchString
* @return the entire collection if the infohash matches, otherwise a collection with only the files that matched
*/
private FileCollection checkKeyWordMatch(String searchString) {
logger.fine("checking for keyword match: " + searchString);
// System.err.println("checking for keyword match: " + searchString);
String[] termSplit = quoteRespectingSplit(searchString);
boolean containsKeyWordEnd = containsKeyword(searchString);
// if the search term doesn't contain any special chars, return true
if (!containsKeyWordEnd) {
return this;
}
logger.finer(searchString + " contains keyword");
//System.err.println(searchString + " contains keyword");
/*
* if it matches the infohash, return the entire thing
*/
for (String s : termSplit) {
for (String keyWordEnd : KEYWORDENDINGS) {
if (s.contains("id" + keyWordEnd)) {
logger.finest(s + " contains " + keyWordEnd);
//System.err.println(s + " contains " + keyWordEnd);
String[] idSplit = s.split(keyWordEnd);
if (idSplit.length == 2) {
String id = idSplit[1];
if (id.equals(this.uniqueID)) {
logger.fine("id match");
//System.err.println("id match");
return this;
}
}
logger.finest("id does not match: " + this.uniqueID + " != " + s);
//System.err.println("id does not match: " + this.uniqueID + " != " + s);
return null;
}
}
}
Set<FileListFile> keywordMatches = new HashSet<FileListFile>();
/*
* or match one of the files
*/
for (FileListFile f : children) {
if (f.keyWordMatch(termSplit)) {
keywordMatches.add(f);
}
}
if (keywordMatches.size() == 0) {
return null;
}
FileCollection hits = this.clone();
hits.setChildren(new ArrayList<FileListFile>(keywordMatches));
return hits;
}
public static boolean containsKeyword(String searchString) {
boolean containsKeyWordEnd = false;
for (String keyWordEnd : KEYWORDENDINGS) {
if (searchString.contains(keyWordEnd)) {
containsKeyWordEnd = true;
}
}
return containsKeyWordEnd;
}
public int getFileNum() {
return children.size();
}
public static String removeWhiteSpaceAfteKeyChars(String input) {
for (String keyword : KEYWORDENDINGS) {
input = removeWhiteSpaceAfterKeyword(input, keyword);
}
return input;
}
public static String removeWhiteSpaceAfterKeyword(String input, String keyword) {
boolean hasColon = input.contains(keyword);
if (!hasColon) {
return input;
}
boolean afterColon = false;
StringBuilder b = new StringBuilder();
char[] chars = input.toCharArray();
for (char c : chars) {
if (c == ':') {
afterColon = true;
b.append(c);
} else if (Character.isWhitespace(c)) {
if (afterColon) {
// remove...
continue;
} else {
b.append(c);
}
} else {
b.append(c);
afterColon = false;
}
}
return b.toString();
}
public static String[] quoteRespectingSplit(String input) {
/*
* quick check to see if we need to split, could be a single word
*/
if (!input.contains(" ")) {
return new String[] {
input
};
}
//boolean withinSingle = false;
boolean withinDouble = false;
char[] chars = input.toCharArray();
StringBuilder currentTerm = new StringBuilder();
LinkedList<String> terms = new LinkedList<String>();
for (char c : chars) {
if (c == ' ') {
// space breaks the term, unless we are within quotes,
// then it gets added to the current term
if (withinDouble) {
currentTerm.append(c);
} else {
terms.add(currentTerm.toString());
currentTerm = new StringBuilder();
}
} else if (c == '"' || c == '\'') {
withinDouble = !withinDouble;
} else {
currentTerm.append(c);
}
}
// check if we have any leftovers
if (currentTerm.length() > 0) {
terms.add(currentTerm.toString());
}
return terms.toArray(new String[terms.size()]);
}
public static void main(String[] args) {
String[] split = quoteRespectingSplit(removeWhiteSpaceAfteKeyChars("this is a \"single unit\" test another \"second unit\" and one more 'test test' and id: \"this is a long id\""));
for (String s : split) {
System.out.println(s);
}
String colonFix = removeWhiteSpaceAfteKeyChars("id: bajs");
System.out.println(colonFix);
}
public List<List<String>> getDirectoryTags() {
return directoryTags;
}
public void setDirectoryTags(List<List<String>> directoryTags) {
this.directoryTags = directoryTags;
calcHash();
}
public boolean containsFile(FileListFile f) {
for (FileListFile c : children) {
if (c.equals(f)) {
return true;
}
}
return false;
}
}