package org.limewire.core.impl.library;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.limewire.collection.CharSequenceKeyAnalyzer;
import org.limewire.collection.PatriciaTrie;
import org.limewire.collection.glazedlists.AbstractListEventListener;
import org.limewire.core.api.Category;
import org.limewire.core.api.FilePropertyKey;
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.search.SearchCategory;
import org.limewire.core.api.search.SearchDetails;
import org.limewire.core.api.search.SearchResult;
import org.limewire.inject.EagerSingleton;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import com.google.inject.Inject;
@EagerSingleton
public class FriendLibraries {
private static final Log LOG = LogFactory.getLog(FriendLibraries.class);
private final Map<String, Library> libraries;
FriendLibraries() {
this.libraries = new ConcurrentHashMap<String, Library>();
}
private static class Library {
private final String presenceId;
private final SearchResultTrie suggestionsIndex;
private final SearchResultTrie fileNameIndex;
private final Map<FilePropertyKey, SearchResultTrie> propertiesIndexes;
private final Map<FilePropertyKey, SearchResultTrie> suggestionPropertiesIndexes;
public Library(String presenceId) {
this.presenceId = presenceId;
suggestionsIndex = new SearchResultTrie();
fileNameIndex = new SearchResultTrie();
propertiesIndexes = new ConcurrentHashMap<FilePropertyKey, SearchResultTrie>();
suggestionPropertiesIndexes = new ConcurrentHashMap<FilePropertyKey, SearchResultTrie>();
}
public void clear() {
suggestionsIndex.lock.writeLock().lock();
try {
suggestionsIndex.clear();
} finally {
suggestionsIndex.lock.writeLock().unlock();
}
fileNameIndex.lock.writeLock().lock();
try {
fileNameIndex.clear();
} finally {
fileNameIndex.lock.writeLock().unlock();
}
synchronized(propertiesIndexes) {
propertiesIndexes.clear();
}
synchronized(suggestionPropertiesIndexes) {
suggestionPropertiesIndexes.clear();
}
}
public SearchResultTrie getOrCreateFilePropertyIndex(FilePropertyKey filePropertyKey) {
SearchResultTrie propertiesIndex = propertiesIndexes.get(filePropertyKey);
if (propertiesIndex == null) {
synchronized (propertiesIndexes) {
propertiesIndex = propertiesIndexes.get(filePropertyKey);
if (propertiesIndex == null) {
propertiesIndex = new SearchResultTrie();
propertiesIndexes.put(filePropertyKey, propertiesIndex);
}
}
}
return propertiesIndex;
}
public SearchResultTrie getFilePropertyIndex(FilePropertyKey filePropertyKey) {
return propertiesIndexes.get(filePropertyKey);
}
public SearchResultTrie getOrCreateSuggestionPropertyIndex(FilePropertyKey filePropertyKey) {
SearchResultTrie propertiesIndex = suggestionPropertiesIndexes.get(filePropertyKey);
if (propertiesIndex == null) {
synchronized (suggestionPropertiesIndexes) {
propertiesIndex = suggestionPropertiesIndexes.get(filePropertyKey);
if (propertiesIndex == null) {
propertiesIndex = new SearchResultTrie();
suggestionPropertiesIndexes.put(filePropertyKey, propertiesIndex);
}
}
}
return propertiesIndex;
}
public SearchResultTrie getSuggestionPropertyIndex(FilePropertyKey filePropertyKey) {
return suggestionPropertiesIndexes.get(filePropertyKey);
}
public SearchResultTrie getFileNameIndex() {
return fileNameIndex;
}
public SearchResultTrie getSuggestionsIndex() {
return suggestionsIndex;
}
/**
* Indexes the file name in both the suggestions and fileName indexes.
* The suggestions index only indexes the phrase as a whole. While the
* filename indexes the phrase by breaking it apart into all the words
* within.
*/
private void indexFileName(SearchResult newFile) {
String fileName = newFile.getFileNameWithoutExtension();
if (fileName != null) {
LOG.debugf("adding file {0} for {1}, indexing under:", fileName, presenceId);
getSuggestionsIndex().lock.writeLock().lock();
try {
// indexes the whole file name so suggestions return the
// whole
// name back
getSuggestionsIndex().addWordToIndex(newFile, fileName);
} finally {
getSuggestionsIndex().lock.writeLock().unlock();
}
getFileNameIndex().lock.writeLock().lock();
try {
getFileNameIndex().addPhraseToIndex(newFile, fileName);
} finally {
getFileNameIndex().lock.writeLock().unlock();
}
}
}
/**
* Indexes properties in both the suggestions and properties indexes.
* <p>
* The suggestions index only indexes the phrase as a whole. While the
* filename indexes the phrase by breaking it apart into all the words
* within.
*/
private void indexProperty(SearchResult newFile, FilePropertyKey filePropertyKey, String phrase) {
SearchResultTrie filePropertyIndex = getOrCreateFilePropertyIndex(filePropertyKey);
filePropertyIndex.lock.writeLock().lock();
try {
filePropertyIndex.addWordToIndex(newFile, phrase);
filePropertyIndex.addPhraseToIndex(newFile, phrase);
} finally {
filePropertyIndex.lock.writeLock().unlock();
}
getSuggestionsIndex().lock.writeLock().lock();
try {
// indexes the whole string so suggestions return the whole name back
getSuggestionsIndex().addWordToIndex(newFile, phrase);
} finally {
getSuggestionsIndex().lock.writeLock().unlock();
}
SearchResultTrie suggestionsFilePropertyIndex = getOrCreateSuggestionPropertyIndex(filePropertyKey);
suggestionsFilePropertyIndex.lock.writeLock().lock();
try {
suggestionsFilePropertyIndex.addWordToIndex(newFile, phrase);
} finally {
suggestionsFilePropertyIndex.lock.writeLock().unlock();
}
}
}
@Inject
void register(RemoteLibraryManager remoteLibraryManager) {
remoteLibraryManager.getFriendLibraryList().addListEventListener(
new ListEventListener<FriendLibrary>() {
@Override
public void listChanged(ListEvent<FriendLibrary> listChanges) {
while (listChanges.next()) {
int type = listChanges.getType();
if (type == ListEvent.INSERT) {
FriendLibrary friendLibrary = listChanges.getSourceList().get(
listChanges.getIndex());
new AbstractListEventListener<PresenceLibrary>() {
private final Map<String, LibraryListener> listeners = new HashMap<String, LibraryListener>();
@Override
protected void itemAdded(PresenceLibrary item, int idx,
EventList<PresenceLibrary> source) {
String presenceId = item.getPresence().getPresenceId();
Library library = new Library(presenceId);
LOG.debugf("adding library for presence {0} to index", presenceId);
libraries.put(item.getPresence().getPresenceId(), library);
LibraryListener listener = new LibraryListener(library);
listeners.put(item.getPresence().getPresenceId(), listener);
item.getModel().addListEventListener(listener);
}
@Override
protected void itemRemoved(PresenceLibrary item, int idx,
EventList<PresenceLibrary> source) {
LOG.debugf("removing library for presence {0} from index",
item.getPresence().getPresenceId());
libraries.remove(item.getPresence().getPresenceId());
LibraryListener listener = listeners.remove(item
.getPresence().getPresenceId());
item.getModel().removeListEventListener(listener);
}
@Override
protected void itemUpdated(PresenceLibrary item,
PresenceLibrary priorItem, int idx,
EventList<PresenceLibrary> source) {
}
}.install(friendLibrary.getPresenceLibraryList());
}
}
}
});
}
/** Returns all suggestions for search terms based on the given prefix. */
public Collection<String> getSuggestions(String prefix, SearchCategory category) {
Set<String> matches = new HashSet<String>();
for (Library library : libraries.values()) {
library.getSuggestionsIndex().lock.readLock().lock();
try {
insertMatchingKeysInto(library.getSuggestionsIndex().getPrefixedBy(prefix),
category, matches);
} finally {
library.getSuggestionsIndex().lock.readLock().unlock();
}
}
return matches;
}
public Collection<String> getSuggestions(String prefix, SearchCategory category,
FilePropertyKey filePropertyKey) {
Set<String> matches = new HashSet<String>();
for (Library library : libraries.values()) {
SearchResultTrie propertyStringTree = library
.getSuggestionPropertyIndex(filePropertyKey);
if (propertyStringTree != null) {
propertyStringTree.lock.readLock().lock();
try {
insertMatchingKeysInto(propertyStringTree.getPrefixedBy(prefix), category,
matches);
} finally {
propertyStringTree.lock.readLock().unlock();
}
}
}
return matches;
}
private void insertMatchingKeysInto(Map<String, Collection<SearchResult>> prefixedBy,
SearchCategory category, Collection<String> results) {
if (category == SearchCategory.ALL) {
results.addAll(prefixedBy.keySet());
} else {
for (Map.Entry<String, Collection<SearchResult>> item : prefixedBy.entrySet()) {
if (containsCategory(category, item.getValue())) {
results.add(item.getKey());
}
}
}
}
private boolean containsCategory(SearchCategory category,
Collection<SearchResult> searchResults) {
for (SearchResult item : searchResults) {
if (category == SearchCategory.forCategory(item.getCategory())) {
return true;
}
}
return false;
}
/** Returns all results that match the query. */
public Collection<SearchResult> getMatchingItems(SearchDetails searchDetails) {
Set<SearchResult> matches = standardSearch(searchDetails);
matches = advancedSearch(searchDetails, matches);
if (matches != null) {
return matches;
} else {
return Collections.emptySet();
}
}
/**
* Returns Set of remote file items matching the advanced SearchDetails. If
* it is a valid search but no results match, then an empty set will be
* returned. If the search is not valid, i.e. there is advanced search data.
* then a null set will be returned.
*/
private Set<SearchResult> advancedSearch(SearchDetails searchDetails,
Set<SearchResult> matches) {
SearchCategory category = searchDetails.getSearchCategory();
Map<FilePropertyKey, String> advancedDetails = searchDetails.getAdvancedDetails();
if (advancedDetails != null && advancedDetails.size() > 0) {
for (FilePropertyKey filePropertyKey : advancedDetails.keySet()) {
String phrase = advancedDetails.get(filePropertyKey);
StringTokenizer st = new StringTokenizer(phrase);
while (st.hasMoreElements()) {
Set<SearchResult> keywordMatches = new HashSet<SearchResult>();
String keyword = st.nextToken();
for (Library library : libraries.values()) {
SearchResultTrie propertyStringTrie = library
.getFilePropertyIndex(filePropertyKey);
if (propertyStringTrie != null) {
propertyStringTrie.lock.readLock().lock();
try {
insertMatchingItemsInto(propertyStringTrie.getPrefixedBy(keyword)
.values(), category, keywordMatches, matches);
} finally {
propertyStringTrie.lock.readLock().unlock();
}
}
}
if (matches == null) {
matches = keywordMatches;
} else {
// Otherwise, we're looking for additional keywords
// -- retain only matched ones.
matches.retainAll(keywordMatches);
}
// Optimization: If nothing matched this keyword,
// nothing can be added.
if (matches.isEmpty()) {
return Collections.emptySet();
}
}
}
}
return matches;
}
/**
* Returns Set of remote file items matching the stand SearchDetails. If it
* is a valid search but no results match, then an empty set will be
* returned. If the search is not valid, i.e. there is no query string. then
* a null set will be returned.
*/
private Set<SearchResult> standardSearch(SearchDetails searchDetails) {
String query = searchDetails.getSearchQuery();
SearchCategory category = searchDetails.getSearchCategory();
Set<SearchResult> matches = null;
StringTokenizer st = new StringTokenizer(query);
while (st.hasMoreElements()) {
Set<SearchResult> keywordMatches = new HashSet<SearchResult>();
String keyword = st.nextToken();
for (Library library : libraries.values()) {
library.fileNameIndex.lock.readLock().lock();
try {
insertMatchingItemsInto(library.fileNameIndex.getPrefixedBy(keyword).values(),
category, keywordMatches, matches);
} finally {
library.fileNameIndex.lock.readLock().unlock();
}
for (FilePropertyKey filePropertyKey : FilePropertyKey.getIndexableKeys()) {
SearchResultTrie propertyStringTrie = library
.getFilePropertyIndex(filePropertyKey);
if (propertyStringTrie != null) {
propertyStringTrie.lock.readLock().lock();
try {
insertMatchingItemsInto(propertyStringTrie.getPrefixedBy(keyword)
.values(), category, keywordMatches, matches);
} finally {
propertyStringTrie.lock.readLock().unlock();
}
}
}
}
// If this is the first keyword, just assign matches to it.
if (matches == null) {
matches = keywordMatches;
} else {
// Otherwise, we're looking for additional keywords -- retain
// only matched ones.
matches.retainAll(keywordMatches);
}
// Optimization: If nothing matched this keyword, nothing can be
// added.
if (matches.isEmpty()) {
return Collections.emptySet();
}
}
return matches;
}
private void insertMatchingItemsInto(Collection<Collection<SearchResult>> prefixedBy,
SearchCategory category, Set<SearchResult> storage, Set<SearchResult> allowedItems) {
for (Collection<SearchResult> searchResults : prefixedBy) {
for (SearchResult item : searchResults) {
Category testCategory = item.getCategory();
boolean allowCategory = category == SearchCategory.ALL
|| category == SearchCategory.forCategory(testCategory);
boolean allowItem = allowedItems == null || allowedItems.contains(item);
if (allowCategory && allowItem) {
storage.add(item);
}
}
}
}
private static class SearchResultTrie extends PatriciaTrie<String, Collection<SearchResult>> {
private final ReadWriteLock lock;
public SearchResultTrie() {
super(new CharSequenceKeyAnalyzer());
lock = new ReentrantReadWriteLock();
}
ReadWriteLock getLock() {
return lock;
}
private String canonicalize(final String s) {
return s.toUpperCase(Locale.US).toLowerCase(Locale.US);
}
@Override
public boolean containsKey(Object k) {
return super.containsKey(canonicalize((String) k));
}
@Override
public Collection<SearchResult> get(Object k) {
return super.get(canonicalize((String) k));
}
@Override
public SortedMap<String, Collection<SearchResult>> getPrefixedBy(String key, int offset, int length) {
return super.getPrefixedBy(canonicalize(key), offset, length);
}
@Override
public SortedMap<String, Collection<SearchResult>> getPrefixedBy(String key, int length) {
return super.getPrefixedBy(canonicalize(key), length);
}
@Override
public SortedMap<String, Collection<SearchResult>> getPrefixedBy(String key) {
return super.getPrefixedBy(canonicalize(key));
}
@Override
public SortedMap<String, Collection<SearchResult>> getPrefixedByBits(String key,
int bitLength) {
return super.getPrefixedByBits(canonicalize(key), bitLength);
}
@Override
public SortedMap<String, Collection<SearchResult>> headMap(String toKey) {
return super.headMap(canonicalize(toKey));
}
@Override
public Collection<SearchResult> put(String key, Collection<SearchResult> value) {
return super.put(canonicalize(key), value);
}
@Override
public Collection<SearchResult> remove(Object k) {
return super.remove(canonicalize((String) k));
}
@Override
public Entry<String, Collection<SearchResult>> select(String key,
Cursor<? super String, ? super Collection<SearchResult>> cursor) {
return super.select(canonicalize(key), cursor);
}
@Override
public Collection<SearchResult> select(String key) {
return super.select(canonicalize(key));
}
@Override
public SortedMap<String, Collection<SearchResult>> subMap(String fromKey, String toKey) {
return super.subMap(canonicalize(fromKey), canonicalize(toKey));
}
@Override
public SortedMap<String, Collection<SearchResult>> tailMap(String fromKey) {
return super.tailMap(canonicalize(fromKey));
}
/**
* Adds the given word to the index as a whole.
*/
public void addWordToIndex(SearchResult newFile, String word) {
LOG.debugf("\t {0}", word);
Collection<SearchResult> filesForWord;
filesForWord = get(word);
if (filesForWord == null) {
filesForWord = new ArrayList<SearchResult>(1);
put(word, filesForWord);
}
filesForWord.add(newFile);
}
/**
* Takes the given phrase and tokenizes it by spaces. Each individual
* token gets added to the index.
*/
public void addPhraseToIndex(SearchResult newFile, String phrase) {
StringTokenizer st = new StringTokenizer(phrase);
while (st.hasMoreElements()) {
String word = st.nextToken();
addWordToIndex(newFile, word);
}
}
}
/**
* Listens to events on a specific presence and updates the library index
* based on these events.
*/
private static class LibraryListener implements ListEventListener<SearchResult> {
private final Library library;
LibraryListener(Library library) {
this.library = library;
}
public void listChanged(ListEvent<SearchResult> listChanges) {
// optimization: if we know the ultimate list is size 0, clear & exit
if(listChanges.getSourceList().size() == 0) {
library.clear();
} else {
while (listChanges.next()) {
switch(listChanges.getType()) {
case ListEvent.INSERT:
SearchResult newFile = listChanges.getSourceList().get(listChanges.getIndex());
index(newFile);
break;
case ListEvent.DELETE:
case ListEvent.UPDATE:
// TODO: if glazedlists supported retrieving the removed items,
// we could just update the one element -- instead,
// we need to rebuild the whole thing.
rebuild(listChanges.getSourceList());
return;
}
}
}
}
private void rebuild(List<SearchResult> files) {
library.clear();
for(SearchResult item : files) {
index(item);
}
}
/**
* Indexes the files properties and name.
*/
private void index(SearchResult newFile) {
library.indexFileName(newFile);
for (FilePropertyKey filePropertyKey : FilePropertyKey.getIndexableKeys()) {
Object property = newFile.getProperty(filePropertyKey);
if (property != null) {
String sentence = property.toString();
library.indexProperty(newFile, filePropertyKey, sentence);
}
}
}
}
}