/*
* Copyright 2004 - 2009 Christian Sprajc. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation.
*
* PowerFolder is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id$
*/
package de.dal33t.powerfolder.net;
import java.lang.Thread.State;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import de.dal33t.powerfolder.Constants;
import de.dal33t.powerfolder.Controller;
import de.dal33t.powerfolder.Member;
import de.dal33t.powerfolder.PFComponent;
import de.dal33t.powerfolder.event.NodeManagerAdapter;
import de.dal33t.powerfolder.event.NodeManagerEvent;
import de.dal33t.powerfolder.event.NodeManagerListener;
import de.dal33t.powerfolder.light.MemberInfo;
import de.dal33t.powerfolder.message.Message;
import de.dal33t.powerfolder.message.SearchNodeRequest;
import de.dal33t.powerfolder.security.SecurityManager;
import de.dal33t.powerfolder.security.SecurityManagerClient;
import de.dal33t.powerfolder.util.Reject;
/**
* This class searches nodes matching a given pattern.
* <p>
* TODO Check if search request should really send to LAN nodes.
*
* @author Dennis "Dante" Waldherr
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc</a>
*/
public class NodeSearcher extends PFComponent {
private String pattern;
/** indicates that we want to interrupt a search */
private boolean stopSearching;
/** exclude friends from this search result */
private boolean ignoreFriends;
/** exclude offline users from search result */
private boolean hideOffline;
private final Thread searchThread;
private NodeManagerListener nodeListener;
private Queue<Member> canidatesFromSupernodes;
private List<Member> searchResultListModel;
private NodeSearchFilter nodeSearchFilter;
/**
* Constructs a new node searcher with giben pattern and a result listmodel.
* <p>
* If you want to get informed about the changes in the list it is
* recommended to use a <code>ObservableList</code>.
* <p>
* Changes of the list model will be done in a own thread (not swing event
* dispatcher thread!)
*
* @param controller
* @param pattern
* @param resultListModel
* the list that will contain the results of the search.
* @param ignoreFriends
* if true friends will be ignored in this list
* @param hideOffline
* hides the users that are offline
*/
public NodeSearcher(Controller controller, String pattern,
List<Member> resultListModel, boolean ignoreFriends, boolean hideOffline)
{
super(controller);
Reject.ifNull(resultListModel, "Result list model is null");
Reject.ifBlank(pattern, "The search pattern is blank");
nodeListener = new MyNodeManagerListener();
searchThread = new Thread(new Searcher(),
"NodeSearcher - searching for " + pattern);
searchThread.setDaemon(true);
this.pattern = pattern;
canidatesFromSupernodes = new LinkedList<Member>();
searchResultListModel = resultListModel;
this.ignoreFriends = ignoreFriends;
this.hideOffline = hideOffline;
nodeSearchFilter = new NodeSearchFilter();
}
/**
* Starts the search
*/
public void start() {
searchThread.start();
}
/**
* Cancels this search. This method will block until the searching has
* stopped.
*/
public void cancelSearch() {
try {
stopSearching = true;
synchronized (searchThread) {
// Wake the searcher Thread
searchThread.notifyAll();
}
// give some time to shutdown the running search
searchThread.join(500);
if (isSearching()) { // Searching didn't stop within 500ms
// Interrupt it
searchThread.interrupt();
}
} catch (InterruptedException ie) {
// This might mean that 2 Threads called cancelSearch()
logSevere("InterruptedException", ie);
}
}
/**
* @return true if this Thread is still searching new members
*/
public boolean isSearching() {
return searchThread.getState() != State.TERMINATED;
}
// Internal code **********************************************************
private boolean checkMember(Member member) {
// /logWarning("Checking: " + member + " - " + member.getAccountInfo());
if (hideOffline && !member.isConnectedToNetwork()) {
return false;
}
if (ignoreFriends && member.isFriend()) {
return false;
}
if (!member.isOnSameNetwork()) {
return false;
}
if (member.isMySelf()) {
return false;
}
return !searchResultListModel.contains(member);
}
private class NodeSearchFilter implements NodeFilter {
public boolean shouldAddNode(MemberInfo nodeInfo) {
return nodeInfo.matches(pattern);
}
}
/**
* Listens to the nodemanager for fresh canidates.
*/
private class MyNodeManagerListener extends NodeManagerAdapter {
public void nodeRemoved(NodeManagerEvent e) {
}
public void nodeAdded(NodeManagerEvent e) {
synchronized (searchThread) {
synchronized (canidatesFromSupernodes) {
canidatesFromSupernodes.add(e.getNode());
}
searchThread.notifyAll();
}
}
public void nodeConnected(NodeManagerEvent e) {
// if connected and hiding of offline is enabled the should popup in
// search result
synchronized (searchThread) {
synchronized (canidatesFromSupernodes) {
canidatesFromSupernodes.add(e.getNode());
}
searchThread.notifyAll();
}
}
public void nodeDisconnected(NodeManagerEvent e) {
if (hideOffline) {
searchResultListModel.remove(e.getNode());
}
}
public void friendAdded(NodeManagerEvent e) {
}
public void friendRemoved(NodeManagerEvent e) {
// if friend status removed it should popup in search result (rare)
synchronized (searchThread) {
synchronized (canidatesFromSupernodes) {
canidatesFromSupernodes.add(e.getNode());
}
searchThread.notifyAll();
}
}
public boolean fireInEventDispatchThread() {
return false;
}
}
/**
* The actual search is done here.
*/
private class Searcher implements Runnable {
public void run() {
searchResultListModel.clear();
// Search local database first
searchLocal();
// Ask server
searchServer();
// Ask connected SuperNodes for search results
searchSupernodes();
}
private void searchLocal() {
for (Member member : getController().getNodeManager()
.getNodesAsCollection())
{
if (checkMember(member) && member.matches(pattern)) {
searchResultListModel.add(member);
}
}
fetchAccountInfos(searchResultListModel);
}
private void searchSupernodes() {
// Listen for fresh canidates
getController().getNodeManager().addNodeManagerListener(
nodeListener);
getController().getNodeManager().addNodeFilter(nodeSearchFilter);
Message msg = new SearchNodeRequest(pattern);
getController().getNodeManager().broadcastMessageToSupernodes(msg,
Constants.N_SUPERNODES_TO_CONTACT_FOR_NODE_SEARCH);
getController().getNodeManager().broadcastMessageLANNodes(msg,
Constants.N_LAN_NODES_TO_CONTACT_FOR_NODE_SEARCH);
try {
while (!stopSearching) {
while (!canidatesFromSupernodes.isEmpty()) {
Member node;
synchronized (canidatesFromSupernodes) {
node = canidatesFromSupernodes.poll();
}
if (node != null && checkMember(node)
&& node.matches(pattern))
{
searchResultListModel.add(node);
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
return;
}
}
fetchAccountInfos(searchResultListModel);
synchronized (searchThread) {
try {
searchThread.wait();
} catch (InterruptedException e) {
logFine("Search was interrupted", e);
break;
}
}
}
} finally {
getController().getNodeManager().removeNodeManagerListener(
nodeListener);
getController().getNodeManager().removeNodeFilter(
nodeSearchFilter);
synchronized (searchThread) {
searchThread.notifyAll();
}
}
}
private void searchServer() {
if (!getController().getOSClient().isConnected()) {
return;
}
if (!getController().getOSClient().isLoggedIn()) {
return;
}
try {
Collection<MemberInfo> res = getController().getOSClient()
.getSecurityService().searchNodes(pattern);
for (MemberInfo nodeInfo : res) {
Member node = nodeInfo.getNode(getController(), true);
if (checkMember(node)) {
searchResultListModel.add(node);
}
}
fetchAccountInfos(searchResultListModel);
} catch (Exception e) {
logWarning("Unable to search via server. " + e);
logFiner(e);
}
}
/**
* Gives the cache a hit.
*
* @param nodes
*/
private void fetchAccountInfos(Collection<Member> nodes) {
SecurityManager secMan = getController().getSecurityManager();
if (secMan instanceof SecurityManagerClient) {
SecurityManagerClient secManClient = (SecurityManagerClient) secMan;
secManClient.fetchAccountInfos(nodes, false);
}
}
}
}