/*
This file is part of leafdigital leafChat.
leafChat 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, either version 3 of the License, or
(at your option) any later version.
leafChat 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 leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2012 Samuel Marshall.
*/
package com.leafdigital.ircui;
import java.util.*;
import java.util.regex.*;
import javax.swing.SwingUtilities;
import util.xml.*;
import com.leafdigital.irc.api.*;
import com.leafdigital.prefs.api.*;
import com.leafdigital.ui.api.*;
import com.leafdigital.ui.api.TreeBox.Item;
import leafchat.core.api.*;
/** Connect icon and dialog. */
@UIHandler("connect")
public class ConnectTool implements SimpleTool, TreeBox.MultiSelectionHandler
{
private static final String PREFS_SINGLE="single";
private static final String PREFS_LIST="list";
private static final String PREFS_LASTCONNECT="lastconnect";
private static final String PREFS_AUTOSHOW="show-connect";
private static final String PREFS_ANDCONNECT="and-connect";
private PluginContext context;
private IRCUIPlugin plugin;
private Window window=null;
/** Checkbox controlling whether dialog shows on launch */
public CheckBox autoShowUI;
/** Checkbox controlling whether to automatically connect on launch too */
public CheckBox andConnectUI;
/** Server tree-list */
public TreeBox serverselectUI;
/** Tab panel */
public TabPanel tabsUI;
/** Button that you can click while connecting to return to the dialog */
public Button backUI;
/** Connect button */
public Button connectUI;
/** Server name box */
public EditBox serverUI;
/** Disconnect list */
public ListBox disconnectlistUI;
/** Disconnect button */
public Button disconnectUI;
/** UI: Choice panel for switching page */
public ChoicePanel choiceUI;
private String directHost;
private int directPort;
private PreferencesGroup directServer;
private boolean direct=false;
private boolean autoShow=false;
private PrefsServerPage ps;
private Item[] selectedServers=null;
/**
* @param pc Context
* @param iup Plugin
* @throws GeneralException
*/
public ConnectTool(PluginContext pc,IRCUIPlugin iup) throws GeneralException
{
this.context=pc;
this.plugin=iup;
pc.requestMessages(SystemStateMsg.class,this);
}
@Override
public void removed()
{
if(window!=null) window.close();
}
/**
* System message: Used to automatically pull up the dialog on launch.
* @param msg Message
* @throws GeneralException
*/
public void msg(SystemStateMsg msg) throws GeneralException
{
if(msg.getType()==SystemStateMsg.UIREADY)
{
Preferences p=context.getSingle(Preferences.class);
if(IRCUIPlugin.USEFAKESERVER || p.toBoolean(p.getGroup(plugin).get(PREFS_AUTOSHOW,"f")))
{
try
{
autoShow=true;
clicked();
}
finally
{
autoShow=false;
}
}
}
}
@Override
public String getLabel()
{
return "Connect";
}
@Override
public String getThemeType()
{
return "connectButton";
}
@Override
public int getDefaultPosition()
{
return 100;
}
@Override
public void clicked() throws GeneralException
{
if(retrieveWindow(false))
{
Preferences p=context.getSingle(Preferences.class);
if(IRCUIPlugin.USEFAKESERVER ||
(autoShow && p.toBoolean(p.getGroup(plugin).get(PREFS_ANDCONNECT,"f"))
&& connectUI.isEnabled())) actionConnect();
}
}
/**
* Either creates a new window or makes sure the existing one is active.
* @param direct True if window is being opened for a direct connection
* @return True if the window is on its UI page; false if it's on the
* 'connecting' page and shouldn't be interrupted
* @throws GeneralException
*/
private boolean retrieveWindow(boolean direct) throws GeneralException
{
if(window == null)
{
UI u = context.getSingle(UI.class);
window = u.createWindow("connect", this);
initWindow(direct);
window.show(false);
}
else
{
if(direct)
{
makeDirect();
}
window.activate();
// Don't do anything if currently connecting
if(!choiceUI.getDisplayed().equals("ui")) return false;
}
return true;
}
void directConnect(String host,int port) throws GeneralException
{
if(retrieveWindow(true))
{
directHost=host;
directPort=port;
directServer=null;
actionConnect();
}
}
void directConnect(PreferencesGroup server) throws GeneralException
{
if(retrieveWindow(true))
{
directHost=null;
directPort=0;
directServer=server;
actionConnect();
}
}
/**
* When a connect window already exists, turns it into 'direct' mode to use
* for a programmatic connection.
* @throws GeneralException
*/
private void makeDirect() throws GeneralException
{
this.direct=true;
backUI.setVisible(false);
}
private void initWindow(boolean direct) throws GeneralException
{
this.direct=direct;
window.setRemember("tool","connect");
if(direct)
{
backUI.setVisible(false);
return;
}
ps=new PrefsServerPage(context);
TabPanel tp=(TabPanel)window.getWidget("tabs");
tp.add(ps.getPage());
serverselectUI.setHandler(this);
Preferences p=context.getSingle(Preferences.class);
PreferencesGroup pg=p.getGroup(plugin).getChild(PREFS_LASTCONNECT);
PreferencesGroup[] apgList=pg.getChild(PREFS_LIST).getAnon();
if(apgList.length==0)
{
PreferencesGroup pgSingle=pg.getChild(PREFS_SINGLE);
if(pgSingle.exists("host") && pgSingle.exists("port"))
{
String sHost=pgSingle.get("host");
int iPort=p.toInt(pgSingle.get("port"));
String server = sHost + (iPort==6667 ? "" : ":"+iPort);
if(pgSingle.exists("channel"))
{
server = "irc://" + server + "/" + pgSingle.get("channel");
if(pgSingle.exists("key"))
{
server += "?" + pgSingle.get("key");
}
}
serverUI.setValue(server);
}
}
else
{
List<TreeBox.Item> l = new LinkedList<TreeBox.Item>();
findItems(l,apgList,(PrefsServerItem)getRoot());
TreeBox.Item[] selected = l.toArray(new TreeBox.Item[l.size()]);
serverselectUI.select(selected);
selected(selected);
}
autoShowUI.setChecked(p.toBoolean(p.getGroup(plugin).get(PREFS_AUTOSHOW,"f")));
andConnectUI.setEnabled(autoShowUI.isChecked());
andConnectUI.setChecked(p.toBoolean(p.getGroup(plugin).get(PREFS_ANDCONNECT,"f")));
// Currently-connected servers
Connections c=context.getSingle(Connections.class);
Server[] as=c.getConnected();
for(int i=0;i<as.length;i++)
{
String sHost=as[i].getReportedHost();
if(sHost==null) sHost=as[i].getConnectedHost();
disconnectlistUI.addItem(sHost);
}
changeAddress();
}
private void findItems(List<TreeBox.Item> l, PreferencesGroup[] apgList,
PrefsServerItem psi) throws GeneralException
{
if(psi.isServer())
{
for(int i=0;i<apgList.length;i++)
{
if(apgList[i].get("host","").equals(psi.getGroup().get("host")))
{
l.add(psi);
break;
}
}
}
else if(psi.isNetwork())
{
for(int i=0;i<apgList.length;i++)
{
if(apgList[i].get("network","").equals(psi.getGroup().get("network")))
{
l.add(psi);
break;
}
}
}
PrefsServerItem[] apsi=(PrefsServerItem[])psi.getChildren();
for(int i=0;i<apsi.length;i++)
{
findItems(l,apgList,apsi[i]);
}
}
/** Callback: Cancel button */
@UIAction
public void actionCancel()
{
cancelConnect();
// This check shouldn't be necessary, but some users hit NPEs at this point
if(window != null)
{
window.close();
}
}
/** Callback: Window closed */
@UIAction
public void windowClosed()
{
window = null;
autoShowUI = null;
andConnectUI = null;
serverselectUI = null;
tabsUI = null;
backUI = null;
connectUI = null;
serverUI = null;
choiceUI = null;
disconnectlistUI = null;
disconnectUI = null;
}
private static class ServerInfo
{
ServerInfo()
{
}
ServerInfo(PreferencesGroup pg)
{
this.pg=pg;
host=pg.get(IRCPrefs.PREF_HOST);
String portRange=pg.getAnonHierarchical(IRCPrefs.PREF_PORTRANGE,
IRCPrefs.PREFDEFAULT_PORTRANGE);
failureRun=pg.getPreferences().toInt(pg.get(IRCPrefs.PREF_FAILURES,"0"));
try
{
int dash=portRange.indexOf('-');
if(dash==-1)
{
portMin=Integer.parseInt(portRange);
portMax=portMin;
}
else
{
portMin=Integer.parseInt(portRange.substring(0,dash));
portMax=Integer.parseInt(portRange.substring(dash+1));
}
if(portMin>portMax)
{
portMax=portMin;
}
}
catch(NumberFormatException e)
{
portMin=6667;
portMax=6667;
}
}
void markError()
{
if(pg!=null)
{
pg.set(IRCPrefs.PREF_FAILURES,pg.getPreferences().fromInt(failureRun+1));
}
}
void markOK()
{
if(pg!=null)
{
pg.unset(IRCPrefs.PREF_FAILURES);
}
}
/** Single server */
String host=null;
/** Port */
int portMin,portMax;
private PreferencesGroup pg;
/**
* Failure run size (1 = last attempt was failure, 2 = last two attempts were
* failures, etc.)
*/
int failureRun=0;
/** If we are supposed to try a sequence of servers in order, they go in here */
ServerInfo[] oneOf=null;
}
private final static Pattern REGEX_IRCURL = Pattern.compile(
"^irc://([a-z0-9-.]+)(?::([0-9]{1,5}))?(?:/(?:([#&+]?)([^?]+)(?:\\?(.+))?)?)?$",
Pattern.CASE_INSENSITIVE);
private final static Pattern REGEX_SERVERNAME = Pattern.compile(
"^([a-z0-9-.]+)(?:[: ]([0-9]{1,5}))?$", Pattern.CASE_INSENSITIVE);
/**
* Callback: Connect button.
* @throws GeneralException
*/
@UIAction
public void actionConnect() throws GeneralException
{
List<ServerInfo> connectTo = new LinkedList<ServerInfo>();
if(direct)
{
ServerInfo si=new ServerInfo();
if(directHost!=null)
{
// Connect to specified server
si.host=directHost;
si.portMin=directPort;
si.portMax=directPort;
connectTo.add(si);
}
else
{
// Connect to server/net from preferences
addSelectedServer(connectTo, null,
new PrefsServerItem(directServer, null, context));
}
}
else
{
Preferences p=context.getSingle(Preferences.class);
PreferencesGroup pg=p.getGroup(plugin).getChild(PREFS_LASTCONNECT);
PreferencesGroup pgList=pg.getChild(PREFS_LIST);
pgList.clearAnon();
if(selectedServers!=null && selectedServers.length>0 && !IRCUIPlugin.USEFAKESERVER)
{
for(int i=0;i<selectedServers.length;i++)
{
PrefsServerItem psi=(PrefsServerItem)selectedServers[i];
addSelectedServer(connectTo,pgList,psi);
}
}
else
{
// Set up server info with default port
ServerInfo si = new ServerInfo();
si.portMin = 6667;
si.portMax = 6667;
String channel = null;
String key = null;
// Check input
String input = serverUI.getValue().trim();
Matcher m = REGEX_IRCURL.matcher(input);
if(IRCUIPlugin.USEFAKESERVER)
{
// Fake (local) server for debugging
si.host="localhost";
}
else if(m.matches())
{
// IRC URL
si.host = m.group(1);
if(m.group(2) != null && m.group(2).length()>0)
{
si.portMin = Integer.parseInt(m.group(2));
si.portMax = si.portMin;
}
if(m.group(4) != null && m.group(4).length()>0)
{
String prefix = "#";
if(m.group(3) != null && m.group(3).length()>0)
{
prefix = m.group(3);
}
channel = prefix + m.group(4);
key = m.group(5); // May be null
if(key == null)
{
key = "";
}
plugin.addJoinRequest(si.host, channel, key);
}
}
else
{
// Server name/port
m = REGEX_SERVERNAME.matcher(input);
if(!m.matches())
{
throw new GeneralException("Invalid input '" + input
+ "' (was supposed to be detected earlier)");
}
si.host = m.group(1);
if(m.group(2) != null && m.group(2).length()>0)
{
si.portMin = Integer.parseInt(m.group(2));
si.portMax = si.portMin;
}
}
PreferencesGroup pgSingle=pg.getChild(PREFS_SINGLE);
pgSingle.set("host",si.host);
pgSingle.set("port",""+si.portMin);
if(channel != null)
{
pgSingle.set("channel", channel);
if(key != null && key.length() > 0)
{
pgSingle.set("key", key);
}
else
{
pgSingle.unset("key");
}
}
else
{
pgSingle.unset("channel");
pgSingle.unset("key");
}
connectTo.add(si);
}
connectUI.setEnabled(false);
}
choiceUI.display("connecting");
final ServerInfo[] connectArray = connectTo.toArray(new ServerInfo[connectTo.size()]);
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
connecting=true;
connect(connectArray);
}
finally
{
connecting=false;
}
}
}).start();
}
/**
* Adds a selected server item to the connectionlist
* @param connectTo
* @param pgList
* @param psi
*/
private void addSelectedServer(List<ServerInfo> connectTo,
PreferencesGroup pgList, PrefsServerItem psi)
{
if(psi.isServer())
{
ServerInfo si=new ServerInfo(psi.getGroup());
if(pgList!=null)
{
PreferencesGroup pgNew=pgList.addAnon();
pgNew.set("host",si.host);
}
connectTo.add(si);
}
else if(psi.isNetwork())
{
if(pgList!=null)
{
PreferencesGroup pgNew=pgList.addAnon();
pgNew.set("network",psi.getGroup().get(IRCPrefs.PREF_NETWORK));
}
ServerInfo si=new ServerInfo();
si.oneOf=new ServerInfo[psi.getChildren().length];
for(int child=0;child<si.oneOf.length;child++)
{
PrefsServerItem childItem=(PrefsServerItem)psi.getChildren()[child];
si.oneOf[child]=new ServerInfo(childItem.getGroup());
}
// Shuffle list
Arrays.sort(si.oneOf,new Comparator<ServerInfo>()
{
private Map<ServerInfo, Double> values = new HashMap<ServerInfo, Double>();
private double getValue(ServerInfo o)
{
Double d = values.get(o);
if(d==null)
{
// Make it less likely to pick the server by adding 0.33 to its
// random number for each recent failure, up to a total 0.99 added,
// which should make it pretty unlikely to come first.
int failures=o.failureRun;
failures=Math.min(failures,3);
d=new Double(Math.random()+0.33*failures);
values.put(o,d);
}
return d.doubleValue();
}
@Override
public int compare(ServerInfo o1, ServerInfo o2)
{
return getValue(o1) > getValue(o2) ? 1 : -1;
}
});
connectTo.add(si);
}
}
private boolean connecting=false,cancelling=false;
private void cancelConnect()
{
// A bit lame
cancelling=true;
while(connecting)
{
try
{
Thread.sleep(250);
}
catch(InterruptedException ie)
{
}
}
cancelling=false;
}
/**
* Callback: Back button.
* @throws GeneralException
*/
@UIAction
public void actionBack() throws GeneralException
{
cancelConnect();
choiceUI.display("ui");
((Button)window.getWidget("connect")).setEnabled(true);
}
private void addTextInSwing(final String xml)
{
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
try
{
if(window==null) return;
TextView tv=(TextView)window.getWidget("log");
tv.addXML(xml);
}
catch(GeneralException e)
{
ErrorMsg.report("Error adding text to connect log",e);
}
}
});
}
private void connect(ServerInfo[] servers)
{
boolean anyErrors=false;
outer: for(int i=0;i<servers.length;i++)
{
if(servers[i].oneOf==null)
{
ServerInfo thisServer=servers[i];
int thisPort=(int)(Math.random()*(thisServer.portMax-thisServer.portMin+1))+thisServer.portMin;
try
{
addTextInSwing("<connect><line>Connecting to <key>"+
XML.esc(thisServer.host)+":"+thisPort+"</key>...</line></connect>");
connect(thisServer.host,thisPort);
if(cancelling) return;
thisServer.markOK();
}
catch(GeneralException e)
{
anyErrors=true;
thisServer.markError();
addTextInSwing("<connect><indent><line>Connection failed! <error>"+
XML.esc(e.getMessage())+"</error></line></indent></connect>");
}
}
else
{
for(int j=0;j<servers[i].oneOf.length;j++)
{
ServerInfo thisServer=servers[i].oneOf[j];
int thisPort=(int)(Math.random()*(thisServer.portMax-thisServer.portMin+1))+thisServer.portMin;
try
{
addTextInSwing("<connect><line>Connecting to <key>"+
XML.esc(thisServer.host)+":"+thisPort+"</key>...</line></connect>");
connect(thisServer.host,thisPort);
if(cancelling) return;
thisServer.markOK();
continue outer; // If we connected to one, all good! on to the next one
}
catch(GeneralException e)
{
thisServer.markError();
addTextInSwing("<connect><indent><line>Connection failed! <error>"+
XML.esc(e.getMessage())+"</error></line></indent></connect>");
}
}
anyErrors=true; // Failed to connect to any of them...
}
}
if(!anyErrors)
{
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
window.close();
}
});
}
}
private void connect(String sServer,int iPort) throws GeneralException
{
Server s=context.getSingle(Connections.class).newServer();
synchronized(s)
{
s.beginConnect(sServer,iPort,new Server.ConnectionProgress()
{
@Override
public void progress(String text)
{
addTextInSwing("<connect><indent><line>"+text+"</line></indent></connect>");
}
});
while(true)
{
if(s.isConnected())
{
return;
}
if(s.getError()!=null)
{
throw s.getError();
}
try
{
s.wait();
}
catch(InterruptedException e)
{
}
}
}
}
/**
* Callback: Server selected for disconnect.
* @throws GeneralException
*/
@UIAction
public void selectionDisconnect() throws GeneralException
{
disconnectUI.setEnabled(disconnectlistUI.getMultiSelected().length > 0);
}
/**
* Callback: Disconnect button.
* @throws GeneralException
*/
@UIAction
public void actionDisconnect() throws GeneralException
{
String[] hosts = disconnectlistUI.getMultiSelected();
if(hosts.length==0) return;
Connections c = context.getSingle(Connections.class);
Server[] connected = c.getConnected();
for(int disconnect=0; disconnect<hosts.length; disconnect++)
{
// Find server
for(int server=0; server<connected.length; server++)
{
String host = connected[server].getReportedHost();
if(host==null)
{
host = connected[server].getConnectedHost();
}
if(host.equals(hosts[disconnect]))
{
connected[server].disconnectGracefully();
}
}
}
window.close();
}
/**
* Callback: Auto-show option changed.
* @throws GeneralException
*/
@UIAction
public void changeAutoShow() throws GeneralException
{
Preferences p=context.getSingle(Preferences.class);
p.getGroup(plugin).set(PREFS_AUTOSHOW,p.fromBoolean(autoShowUI.isChecked()));
andConnectUI.setEnabled(autoShowUI.isChecked());
}
/**
* Callback: Auto-connect option changed.
* @throws GeneralException
*/
@UIAction
public void changeAndConnect() throws GeneralException
{
Preferences p=context.getSingle(Preferences.class);
p.getGroup(plugin).set(PREFS_ANDCONNECT,p.fromBoolean(andConnectUI.isChecked()));
}
@Override
public void selected(Item[] ai)
{
selectedServers=ai;
if(ai!=null && ai.length>0)
{
serverUI.setFlag(EditBox.FLAG_DIM);
}
else
{
serverUI.setFlag(EditBox.FLAG_NORMAL);
}
updateOK();
}
/**
* Callback: Address changed.
*/
@UIAction
public void changeAddress()
{
// If user types in text to the address initially, that counts the same
// as focusing in terms of making that be the default.
if(serverUI.getValue().length()>0)
focusAddress();
updateOK();
}
private boolean checkServerValid()
{
boolean valid = REGEX_IRCURL.matcher(serverUI.getValue()).matches() ||
REGEX_SERVERNAME.matcher(serverUI.getValue()).matches();
serverUI.setFlag(valid ? EditBox.FLAG_NORMAL : EditBox.FLAG_ERROR);
return valid;
}
private void updateOK()
{
// Enable connect button if a server is selected from list, or typed in box
connectUI.setEnabled((selectedServers!=null && selectedServers.length>0) ||
(serverUI.getFlag()!=EditBox.FLAG_DIM && checkServerValid()));
}
/**
* Callback: Address edit focused.
*/
@UIAction
public void focusAddress()
{
// Deselect list so that address is used - if something's in there
if(serverUI.getValue().length() > 0)
{
serverselectUI.select((Item)null);
selected((Item[])null);
checkServerValid();
}
}
@Override
public Item getRoot()
{
return ps.getRoot();
}
@Override
public boolean isRootDisplayed()
{
return false;
}
/**
* Callback: Tab changed.
*/
@UIAction
public void changeTabs()
{
if("connectPage".equals(tabsUI.getDisplayed()))
serverselectUI.update();
}
}