package org.limewire.ui.swing.search.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.limewire.core.api.Category;
import org.limewire.core.api.FilePropertyKey;
import org.limewire.core.api.URN;
import org.limewire.core.api.endpoint.RemoteHost;
import org.limewire.core.api.search.SearchResult;
import org.limewire.friend.api.Friend;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.ui.swing.util.PropertiableFileUtils;
import org.limewire.ui.swing.util.PropertiableHeadings;
import org.limewire.util.Objects;
import com.google.inject.Provider;
/**
* An implementation of VisualSearchResult for displaying actual search
* results.
*/
class SearchResultAdapter implements VisualSearchResult, Comparable {
private static final Log LOG = LogFactory.getLog(SearchResultAdapter.class);
private static final Matcher FIND_HTML_MARKUP;
static {
Pattern p = Pattern.compile("[<][/]?[\\w =\"\\./:#\\-\\!\\&\\?]*[>]");
FIND_HTML_MARKUP = p.matcher("");
}
private static final Comparator<RemoteHost> REMOTE_HOST_COMPARATOR = new RemoteHostComparator();
private static final Comparator<Friend> FRIEND_COMPARATOR = new FriendComparator();
private List<SearchResult> coreResults;
private Set<Friend> friends;
private final Set<RemoteHost> remoteHosts;
private final Provider<PropertiableHeadings> propertiableHeadings;
private BasicDownloadState downloadState = BasicDownloadState.NOT_STARTED;
private CopyOnWriteArrayList<VisualSearchResult> similarResults = null;
private VisualSearchResult similarityParent;
private boolean anonymous;
private boolean visible;
private boolean childrenVisible;
private Boolean spamCache;
private boolean preExistingDownload = false;
private int relevance = 0;
private String cachedHeading;
private String cachedSubHeading;
private final VisualSearchResultStatusListener changeListener;
/**
* Constructs a SearchResultAdapter with the specified List of core results
* and property values.
*/
public SearchResultAdapter(SearchResult source, Provider<PropertiableHeadings> propertiableHeadings,
VisualSearchResultStatusListener changeListener) {
this.propertiableHeadings = propertiableHeadings;
this.remoteHosts = new TreeSet<RemoteHost>(REMOTE_HOST_COMPARATOR);
this.visible = true;
this.childrenVisible = false;
this.changeListener = changeListener;
addNewSource(source);
}
@Override
public boolean isAnonymous() {
return anonymous;
}
@Override
public Category getCategory() {
return coreResults.get(0).getCategory();
}
@Override
public List<SearchResult> getCoreSearchResults() {
return coreResults;
}
@Override
public String getFileExtension() {
return coreResults.get(0).getFileExtension();
}
@Override
public String getFileName() {
return coreResults.get(0).getFileName();
}
@Override
public Collection<Friend> getFriends() {
return friends == null ? Collections.<Friend>emptySet() : friends;
}
@Override
public Object getProperty(FilePropertyKey key) {
// find the first non-null value in any of the search results.
for(SearchResult result : coreResults) {
Object value = result.getProperty(key);
if(value != null) {
return value;
}
}
return null;
}
@Override
public String getPropertyString(FilePropertyKey key) {
Object value = getProperty(key);
if (value != null) {
return value.toString();
} else {
return null;
}
}
@Override
public String getNameProperty(boolean useAudioArtist) {
return PropertiableFileUtils.getNameProperty(this, useAudioArtist);
}
@Override
public void addSimilarSearchResult(VisualSearchResult similarResult) {
assert similarResult != this;
if(similarResults == null) {
similarResults = new CopyOnWriteArrayList<VisualSearchResult>();
}
similarResults.addIfAbsent(similarResult);
}
@Override
public void removeSimilarSearchResult(VisualSearchResult result) {
if(similarResults != null) {
similarResults.remove(result);
}
}
@Override
public List<VisualSearchResult> getSimilarResults() {
return similarResults == null ? Collections.<VisualSearchResult>emptyList() : similarResults;
}
@Override
public void setSimilarityParent(VisualSearchResult parent) {
VisualSearchResult oldParent = this.similarityParent;
this.similarityParent = parent;
firePropertyChange("similarityParent", oldParent, parent);
}
@Override
public VisualSearchResult getSimilarityParent() {
return similarityParent;
}
@Override
public long getSize() {
return coreResults.get(0).getSize();
}
@Override
public Collection<RemoteHost> getSources() {
return remoteHosts;
}
@Override
public BasicDownloadState getDownloadState() {
return downloadState;
}
@Override
public void setDownloadState(BasicDownloadState downloadState) {
// If the download was aborted, recalculate the spam score
if(downloadState == BasicDownloadState.NOT_STARTED) {
boolean oldSpam = isSpam();
spamCache = null;
boolean newSpam = isSpam();
firePropertyChange("spam-core", oldSpam, newSpam);
}
BasicDownloadState oldDownloadState = this.downloadState;
this.downloadState = downloadState;
firePropertyChange("downloadState", oldDownloadState, downloadState);
}
@Override
public String toString() {
return getCoreSearchResults().toString();
}
/**
* Reloads the sources from the core search results. The number of alt-locs
* is limited to avoid giving high relevance to spam results.
*/
void addNewSource(SearchResult result) {
// optimize for only having a single result
if(coreResults == null) {
coreResults = Collections.singletonList(result);
} else {
if(coreResults.size() == 1) {
coreResults = new ArrayList<SearchResult>(coreResults);
}
coreResults.add(result);
}
relevance += result.getRelevance();
// Build collection of non-anonymous friends for filtering.
for (RemoteHost host : result.getSources()) {
remoteHosts.add(host);
Friend friend = host.getFriendPresence().getFriend();
if (friend.isAnonymous()) {
anonymous = true;
} else {
if(friends == null) {
// optimize for a single friend having it
friends = Collections.singleton(friend);
} else {
// convert to TreeSet if we need to.
if(!(friends instanceof TreeSet)) {
Set<Friend> newFriends = new TreeSet<Friend>(FRIEND_COMPARATOR);
newFriends.addAll(friends);
friends = newFriends;
}
friends.add(friend);
}
}
}
}
@Override
public boolean isVisible() {
return visible;
}
@Override
public void setVisible(boolean visible) {
boolean oldValue = this.visible;
this.visible = visible;
firePropertyChange("visible", oldValue, visible);
if(LOG.isDebugEnabled()) {
LOG.debugf("Updating visible to {0} for urn: {1}", visible, getUrn());
}
}
@Override
public boolean isChildrenVisible() {
return childrenVisible;
}
@Override
public boolean isSpam() {
if (spamCache == null) {
boolean spam = false;
for (SearchResult result : coreResults)
spam |= result.isSpam();
spamCache = spam;
}
return spamCache.booleanValue();
}
@Override
public void setSpam(boolean spam) {
boolean oldSpam = isSpam();
spamCache = spam;
firePropertyChange("spam-ui", oldSpam, spam);
}
@Override
public void setChildrenVisible(boolean childrenVisible) {
boolean oldChildrenVisible = childrenVisible;
this.childrenVisible = childrenVisible;
for (VisualSearchResult similarResult : getSimilarResults()) {
similarResult.setVisible(childrenVisible);
similarResult.setChildrenVisible(false);
}
firePropertyChange("childrenVisible", oldChildrenVisible, childrenVisible);
}
@Override
public void toggleChildrenVisibility() {
setChildrenVisible(!isShowingSimilarResults());
}
private boolean isShowingSimilarResults() {
return getSimilarResults().size() > 0 && isChildrenVisible();
}
@Override
public URN getUrn() {
return coreResults.get(0).getUrn();
}
@Override
public URN getNavSelectionId() {
return getUrn();
}
@Override
public String getMagnetLink() {
//TODO: add other search results as alternative results to the magnet.
// A bit too intensive for the release.
if(getCoreSearchResults().size() > 0)
return getCoreSearchResults().get(0).getMagnetURL();
else
return null;
}
@Override
public String getHeading() {
if (cachedHeading == null) {
cachedHeading = sanitize(propertiableHeadings.get().getHeading(this));
}
return cachedHeading;
}
/**
* This method checks for HTML encoding in the content of the supplied string.
* If found, the encoding is stripped from the string and this result is marked as
* spam.
* @param input
* @return The stripped string is returned (or the same string if no HTML encoding
* is found).
*/
private String sanitize(String input) {
Matcher matcher = FIND_HTML_MARKUP.reset(input);
if (matcher.find()) {
setSpam(true);
return matcher.replaceAll("");
}
return input;
}
@Override
public String getSubHeading() {
if (cachedSubHeading == null) {
cachedSubHeading = sanitize(propertiableHeadings.get().getSubHeading(this));
}
return cachedSubHeading;
}
@Override
public int getRelevance() {
return relevance;
}
/**
* If any of the search results' lime xml docs contains a license string
* the entire VisualSearchResult is considered "licensed".
*
*/
@Override
public boolean isLicensed() {
for (SearchResult searchResult : coreResults) {
if (searchResult.isLicensed()) {
return true;
}
}
return false;
}
@Override
public boolean isPreExistingDownload() {
return preExistingDownload;
}
@Override
public void setPreExistingDownload(boolean preExistingDownload) {
this.preExistingDownload = preExistingDownload;
}
@Override
public int compareTo(Object o) {
if(!(o instanceof SearchResultAdapter))
return -1;
SearchResultAdapter sra = (SearchResultAdapter) o;
return getHeading().compareTo(sra.getHeading());
}
private void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
if (oldValue != newValue) {
changeListener.resultChanged(this, propertyName, oldValue, newValue);
}
}
private void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
if (!Objects.equalOrNull(oldValue, newValue)) {
changeListener.resultChanged(this, propertyName, oldValue, newValue);
}
}
private static class RemoteHostComparator implements Comparator<RemoteHost> {
@Override
public int compare(RemoteHost o1, RemoteHost o2) {
int compare = 0;
boolean anonymous1 = o1.getFriendPresence().getFriend().isAnonymous();
boolean anonymous2 = o2.getFriendPresence().getFriend().isAnonymous();
if (anonymous1 == anonymous2) {
compare = o1.getFriendPresence().getFriend().getRenderName().compareToIgnoreCase(o2.getFriendPresence().getFriend().getRenderName());
} else if (anonymous1) {
compare = 1;
} else if (anonymous2) {
compare = -1;
}
return compare;
}
}
private static class FriendComparator implements Comparator<Friend> {
@Override
public int compare(Friend o1, Friend o2) {
String id1 = o1.getId();
String id2 = o2.getId();
return Objects.compareToNullIgnoreCase(id1, id2, false);
}
}
}