package thaw.core;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import freenet.crypt.Yarrow;
import thaw.fcp.FCPClientHello;
import thaw.fcp.FCPConnection;
import thaw.fcp.FCPQueryManager;
import thaw.fcp.FCPQueueLoader;
import thaw.fcp.FCPQueueManager;
import thaw.fcp.FCPWatchGlobal;
import thaw.fcp.FCPMessage;
import thaw.gui.IconBox;
/**
* A "core" contains references to all the main parts of Thaw.
* The Core has all the functions needed to initialize Thaw / stop Thaw.
*/
public class Core implements Observer {
private SplashScreen splashScreen = null;
private MainWindow mainWindow = null;
private Config config = null;
private PluginManager pluginManager = null;
private ConfigWindow configWindow = null;
private FCPConnection connection = null;
private FCPQueryManager queryManager = null;
private FCPQueueManager queueManager = null;
private FCPClientHello clientHello = null;
private String lookAndFeel = null;
public final static int MAX_CONNECT_TRIES = 3;
public final static int TIME_BETWEEN_EACH_TRY = 20000;
private ReconnectionManager reconnectionManager = null;
private static final Random RANDOM = new Yarrow();
private boolean isStopping = false;
/**
* Creates a core, but do nothing else (no initialization).
*/
public Core() {
isStopping = false;
Logger.info(this, "Thaw, version "+Main.VERSION, true);
Logger.info(this, "2006-2008(c) Freenet project", true);
Logger.info(this, "Released under GPL license version 2 or later (see http://www.fsf.org/licensing/licenses/gpl.html)", true);
}
/**
* Gives a ref to the object containing the config.
*/
public Config getConfig() {
return config;
}
/**
* Gives a ref to the object managing the splash screen.
*/
public SplashScreen getSplashScreen() {
return splashScreen;
}
/**
* Gives a ref to the object managing the main window.
*/
public MainWindow getMainWindow() {
return mainWindow;
}
/**
* Gives a ref to the object managing the config window.
*/
public ConfigWindow getConfigWindow() {
return configWindow;
}
/**
* Gives a ref to the plugin manager.
*/
public PluginManager getPluginManager() {
return pluginManager;
}
/**
* Here really start the program.
* @return true is success, false if not
*/
public boolean initAll() {
IconBox.loadIcons();
splashScreen = new SplashScreen();
splashScreen.display();
splashScreen.setProgressionAndStatus(0, "Loading configuration ...");
splashScreen.addIcon(IconBox.settings);
if(!initConfig())
return false;
splashScreen.setProgressionAndStatus(10, "Applying look and feel ...");
if (!initializeLookAndFeel())
return false;
splashScreen.setProgressionAndStatus(20, "Connecting ...");
if(!initConnection())
new thaw.gui.WarningWindow(this, I18n.getMessage("thaw.warning.unableToConnectTo")+
" "+ config.getValue("nodeAddress")+
":"+ config.getValue("nodePort"));
splashScreen.setProgressionAndStatus(40, "Preparing the main window ...");
splashScreen.addIcon(IconBox.mainWindow);
if(!initGraphics())
return false;
splashScreen.setProgressionAndStatus(50, "Loading plugins ...");
if(!initPluginManager())
return false;
splashScreen.setProgressionAndStatus(100, "Ready");
mainWindow.setStatus(IconBox.minDisconnectAction,
"Thaw "+Main.VERSION+" : "+I18n.getMessage("thaw.statusBar.ready"));
splashScreen.hide();
splashScreen = null;
mainWindow.setVisible(true);
setTheme(lookAndFeel);
return true;
}
/**
* Init configuration. May re-set I18n.
*/
public boolean initConfig() {
config = new Config(this, Config.CONFIG_FILE_NAME);
config.loadConfig();
config.setDefaultValues();
if (config.getValue("logLevel") != null)
Logger.setLogLevel(Integer.parseInt(config.getValue("logLevel")));
if (config.getValue("tmpDir") != null)
System.setProperty("java.io.tmpdir", config.getValue("tmpDir"));
if (config.getValue("lang") != null)
I18n.setLocale(new java.util.Locale(config.getValue("lang")));
return true;
}
/**
* My node takes too much time to answer. So now the connection process is partially threaded.
*/
protected class ConnectionProcess implements ThawRunnable {
private Core c;
private boolean running;
public ConnectionProcess(Core c) {
this.c = c;
this.running = true;
}
public void run() {
process();
connectionProcess = null;
}
public void stop() {
running = false;
}
public boolean process() {
boolean ret = true;
clientHello = new FCPClientHello(queryManager, config.getValue("thawId"));
if(!clientHello.start(null)) {
Logger.warning(this, "Id already used or timeout !");
subDisconnect();
ret = false;
} else {
Logger.debug(this, "Hello successful");
Logger.debug(this, "Node name : "+clientHello.getNodeName());
Logger.debug(this, "FCP version : "+clientHello.getNodeFCPVersion());
Logger.debug(this, "Node version : "+clientHello.getNodeVersion());
if (ret)
queueManager.startScheduler();
if (!running) ret = false;
if (ret)
queueManager.restartNonPersistent();
if (!running) ret = false;
if (ret) {
final FCPWatchGlobal watchGlobal = new FCPWatchGlobal(true);
watchGlobal.start(queueManager);
}
if (!running) ret = false;
if (ret) {
final FCPQueueLoader queueLoader = new FCPQueueLoader(config.getValue("thawId"));
queueLoader.start(queueManager);
}
if (!running) ret = false;
}
if(ret && connection.isConnected())
connection.addObserver(c);
if(getMainWindow() != null && running) {
if (ret)
getMainWindow().setStatus(IconBox.minConnectAction,
I18n.getMessage("thaw.statusBar.ready"));
else
getMainWindow().setStatus(IconBox.minDisconnectAction,
I18n.getMessage("thaw.statusBar.disconnected"), java.awt.Color.RED);
}
return ret;
}
}
private ConnectionProcess connectionProcess = null;
/**
* Init the connection to the node.
* If a connection is already established, it will disconnect, so
* if you called canDisconnect() before, then this function can be called safely.
* <br/>
* If the connection is opened successfully, ClientHello will be thread, so you won't have its result
* @see #canDisconnect()
*/
public boolean initConnection() {
boolean ret = true;
if (connectionProcess != null) {
Logger.notice(this, "A connection process is already running");
return false;
}
if(getMainWindow() != null) {
getMainWindow().setStatus(IconBox.blueBunny,
I18n.getMessage("thaw.statusBar.connecting"), java.awt.Color.RED);
}
try {
if(queueManager != null)
queueManager.stopScheduler();
if((connection != null) && connection.isConnected()) {
subDisconnect();
}
if (connection != null)
connection.deleteObserver(this);
connection = new FCPConnection(config.getValue("nodeAddress"),
Integer.parseInt(config.getValue("nodePort")),
Integer.parseInt(config.getValue("maxUploadSpeed")),
Boolean.valueOf(config.getValue("multipleSockets")).booleanValue(),
Boolean.valueOf(config.getValue("sameComputer")).booleanValue(),
Boolean.valueOf(config.getValue("downloadLocally")).booleanValue());
if(!connection.connect()) {
Logger.warning(this, "Unable to connect !");
ret = false;
}
if (queryManager != null)
queryManager.deleteObserver(this);
queryManager = new FCPQueryManager(connection);
queryManager.addObserver(this);
queueManager = new FCPQueueManager(queryManager,
config.getValue("thawId"),
Integer.parseInt(config.getValue("maxSimultaneousDownloads")),
Integer.parseInt(config.getValue("maxSimultaneousInsertions")));
if(ret && connection.isConnected()) {
queryManager.startListening();
QueueKeeper.loadQueue(queueManager, "thaw.queue.xml");
connectionProcess = new ConnectionProcess(this);
Thread th = new ThawThread(connectionProcess, "Connection process", this);
th.start();
}
} catch(final Exception e) { /* A little bit not ... "nice" ... */
Logger.warning(this, "Exception while connecting : "+e.toString()+" ; "+e.getMessage() + " ; "+e.getCause());
e.printStackTrace();
return false;
}
return ret;
}
public FCPConnection getConnectionManager() {
return connection;
}
public FCPQueueManager getQueueManager() {
return queueManager;
}
/**
* FCPClientHello object contains all the information given by the node when the connection
* was initiated.
*/
public FCPClientHello getClientHello() {
return clientHello;
}
/**
* To call before initGraphics() !
* @param lAndF LookAndFeel name
*/
public void setLookAndFeel(final String lAndF) {
this.lookAndFeel = lAndF;
}
private static class LnFSetter implements Runnable {
private Core core;
private String theme;
public LnFSetter(Core c, String t) {
core = c;
theme = t;
}
public void run() {
try {
UIManager.setLookAndFeel(theme);
} catch(ClassNotFoundException e) {
Logger.error(this, "Theme '"+theme+"' not found ! : "+e.toString());
} catch(InstantiationException e) {
Logger.error(this, "(1) Error while loading theme '"+theme+"' : "+e.toString());
} catch(IllegalAccessException e) {
Logger.error(this, "(2) Error while loading theme '"+theme+"' : "+e.toString());
} catch(javax.swing.UnsupportedLookAndFeelException e) {
Logger.error(this, "(3) Error while loading theme '"+theme+"' : "+e.toString());
}
if (core.getMainWindow() != null)
javax.swing.SwingUtilities.updateComponentTreeUI(core.getMainWindow().getMainFrame());
if (core.getConfigWindow() != null)
javax.swing.SwingUtilities.updateComponentTreeUI(core.getConfigWindow().getFrame());
}
}
private void reallySetTheme(String theme) {
if (theme == null)
return;
Logger.notice(this, "Setting theme : "+ theme);
LnFSetter s = new LnFSetter(this, theme);
try {
javax.swing.SwingUtilities.invokeAndWait(s);
} catch(InterruptedException e) {
Logger.error(s, "Interrupted while setting theme '"+theme+"' because: "+e.toString());
} catch(java.lang.reflect.InvocationTargetException e) {
Logger.error(s, "Error while setting theme : "+e.toString());
Logger.notice(s, "Original exception: "+e.getTargetException().toString());
e.getTargetException().printStackTrace();
}
}
public void setTheme(String theme) {
if (theme == null) {
if (getConfig() != null)
theme = getConfig().getValue("lookAndFeel");
if (theme == null)
theme = UIManager.getSystemLookAndFeelClassName();
}
lookAndFeel = theme;
/* the recommandation is to set the lnf before displaying the first window */
/* but I had more bugs with the GTK lnf when I followed the recommandation than
* when I didn't. So now I only change it when the main window is already displayed :p */
if (mainWindow != null
&& mainWindow.getMainFrame() != null
&& mainWindow.getMainFrame().isVisible())
reallySetTheme(lookAndFeel);
}
/**
* This method sets the look and feel specified with setLookAndFeel().
* If none was specified, the System Look and Feel is set.
*/
private boolean initializeLookAndFeel() { /* non static, else I can't call correctly Logger functions */
JFrame.setDefaultLookAndFeelDecorated(false); /* Don't touch my window decorations ! */
JDialog.setDefaultLookAndFeelDecorated(false);
try {
setTheme(this.lookAndFeel);
if (splashScreen != null)
splashScreen.rebuild();
} catch (final Exception e) {
Logger.warning(this, "Exception while setting the L&F : " + e.toString() + " ; " + e.getMessage());
e.printStackTrace();
Logger.warning(this, "Will use the default lookAndFeel");
}
return true;
}
/**
* Init graphics.
*/
public boolean initGraphics() {
//initializeLookAndFeel();
mainWindow = new MainWindow(this);
configWindow = new ConfigWindow(this);
return true;
}
/**
* Init plugin manager.
*/
public boolean initPluginManager() {
pluginManager = new PluginManager(this);
if(!pluginManager.loadAndRunPlugins())
return false;
return true;
}
/**
* End of the world.
*/
public void exit() {
this.exit(false);
}
/**
* Makes things nicely ... :)
*/
public void disconnect() {
if (reconnectionManager != null) {
reconnectionManager.stop();
reconnectionManager = null;
}
subDisconnect();
}
public void subDisconnect() {
Logger.info(this, "Disconnecting");
if (mainWindow != null) {
mainWindow.setStatus(IconBox.minDisconnectAction,
I18n.getMessage("thaw.statusBar.disconnected"), java.awt.Color.RED);
/* not null because we want to force the cleaning */
mainWindow.changeButtonsInTheToolbar(this, new java.util.Vector());
}
if (connection != null) {
connection.deleteObserver(this);
connection.disconnect();
Logger.info(this, "Saving queue state");
QueueKeeper.saveQueue(queueManager, "thaw.queue.xml");
} else {
Logger.warning(this, "No connection ?!");
}
}
/**
* Check if the connection can be interrupted safely.
*/
public boolean canDisconnect() {
return (connection == null) || !connection.isWriting();
}
/**
* End of the world.
* @param force if true, doesn't check if FCPConnection.isWriting().
* @see #exit()
*/
public void exit(boolean force) {
isStopping = true;
if(!force) {
if(!canDisconnect()) {
if(!askDeconnectionConfirmation())
return;
}
}
Logger.info(this, "Stopping scheduler ...");
if(queueManager != null)
queueManager.stopScheduler();
Logger.info(this, "Hidding main window ...");
mainWindow.setVisible(false);
configWindow.setVisible(false);
Logger.info(this, "Stopping plugins ...");
pluginManager.stopPlugins();
disconnect();
Logger.info(this, "Saving configuration ...");
if(!config.saveConfig()) {
Logger.error(this, "Config was not saved correctly !");
}
ThawThread.setAllowFullStop(true);
Logger.info(this, "Threads remaining:");
ThawThread.listThreads();
Logger.info(this, "Stopping all the remaining threads ...");
ThawThread.stopAll();
}
public boolean askDeconnectionConfirmation() {
final int ret = JOptionPane.showOptionDialog((java.awt.Component)null,
I18n.getMessage("thaw.warning.isWriting"),
"Thaw - "+I18n.getMessage("thaw.warning.title"),
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE,
(javax.swing.Icon)null,
(java.lang.Object[])null,
(java.lang.Object)null);
if((ret == JOptionPane.CLOSED_OPTION)
|| (ret == JOptionPane.CANCEL_OPTION)
|| (ret == JOptionPane.NO_OPTION))
return false;
return true;
}
protected class ReconnectionManager implements ThawRunnable {
private boolean running = true;
private boolean initialWait = true;
public ReconnectionManager(boolean initialWait) {
running = true;
this.initialWait = initialWait;
}
public void run() {
synchronized(PluginManager.pluginLock) {
Logger.notice(this, "Starting reconnection process !");
getMainWindow().setStatus(IconBox.blueBunny,
I18n.getMessage("thaw.statusBar.connecting"), java.awt.Color.RED);
getPluginManager().stopPlugins(); /* don't forget there is the status bar plugin */
getMainWindow().setStatus(IconBox.blueBunny,
I18n.getMessage("thaw.statusBar.connecting"), java.awt.Color.RED);
subDisconnect();
while(running && !isStopping()) {
try {
if (initialWait)
Thread.sleep(Core.TIME_BETWEEN_EACH_TRY);
} catch(final java.lang.InterruptedException e) {
// brouzouf
}
initialWait = true;
Logger.notice(this, "Trying to reconnect ...");
if(initConnection())
break;
}
if (running && !isStopping()) {
getMainWindow().setStatus(IconBox.minConnectAction,
I18n.getMessage("thaw.statusBar.ready"));
} else {
getMainWindow().setStatus(IconBox.minDisconnectAction,
I18n.getMessage("thaw.statusBar.disconnected"), java.awt.Color.RED);
}
if (running && !isStopping()) {
getPluginManager().loadAndRunPlugins();
}
reconnectionManager = null;
getMainWindow().connectionHasChanged();
}
}
public void stop() {
Logger.warning(this, "Canceling reconnection ...");
running = false;
}
}
/**
* use Thread => will also do all the work related to the plugins
*/
public void reconnect(boolean withInitialWait) {
synchronized(this) {
if (reconnectionManager == null) {
reconnectionManager = new ReconnectionManager(withInitialWait);
final Thread th = new ThawThread(reconnectionManager,
"Reconnection manager", this);
th.start();
} else {
Logger.warning(this, "Already trying to reconnect !");
}
}
}
public boolean isReconnecting() {
return (reconnectionManager != null);
}
public void askToDisableDDA() {
String text =
I18n.getMessage("thaw.warning.DDA.l0") + "\n" +
I18n.getMessage("thaw.warning.DDA.l1") + "\n" +
I18n.getMessage("thaw.warning.DDA.l2") + "\n" +
I18n.getMessage("thaw.warning.DDA.l3") + "\n" +
I18n.getMessage("thaw.warning.DDA.l4");
text = text.replaceAll("#", I18n.getMessage("thaw.config.sameComputer"));
int ret = JOptionPane.showConfirmDialog(mainWindow.getMainFrame(),
text,
I18n.getMessage("thaw.warning.title"),
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE);
if (ret == JOptionPane.YES_OPTION) {
getConfig().setValue("sameComputer", Boolean.toString(false));
getConnectionManager().setLocalSocket(false);
getConfigWindow().close(true, false);
/* if we are lucky, it's enought */
}
}
public void update(final Observable o, final Object target) {
Logger.debug(this, "Move on the connection (?)");
if ((o == connection) && !connection.isConnected()) {
reconnect(true);
}
if ((o == queryManager) && target instanceof FCPMessage) {
FCPMessage m = (FCPMessage)target;
if ("ProtocolError".equals(m.getMessageName())) {
int code = Integer.parseInt(m.getValue("Code"));
if (connection.isLocalSocket()
&& (code == 8 /* Invalid field (?!) */
|| code == 9 /* File not found */
|| code == 12 /* Couldn't create file */
|| code == 13 /* Couldn't write file */
|| code == 14 /* Couldn't rename file */
|| code == 22 /* File parse error */
|| code == 26 /* Could not read file */)) {
askToDisableDDA();
}
}
}
}
public static Random getRandom() {
return RANDOM;
}
public boolean isStopping() {
return isStopping;
}
/*
* @param major always 1 atm
* @param minor 5, 6, etc, depending of what you want
*/
public static boolean checkJavaVersion(int major, int minor) {
String ver = System.getProperty("java.version");
if (ver == null) {
Logger.notice(ver, "No Jvm version ?!");
return false;
}
Logger.info(ver, "JVM Version : "+ver);
String[] version = ver.split("\\.");
if (version.length < 2) {
Logger.notice(ver, "Can't parse the jvm version !");
return false;
}
if (Integer.parseInt(version[0]) < major)
return false;
if (Integer.parseInt(version[1]) < minor)
return false;
return true;
}
}