/*
* © Copyright 2008 by Edgar Kalkowski (eMail@edgar-kalkowski.de)
*
* This file is part of Erki's ErkiTalk Client.
*
* Erki's ErkiTalk Client 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.
*
* This program 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
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package erki.talk.clients.erki;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import erki.api.util.CommandLineParser;
import erki.api.util.Localizor;
import erki.api.util.Logger;
import erki.talk.clients.erki.event.ConnectEvent;
import erki.talk.clients.erki.event.Event;
import erki.talk.clients.erki.event.EventObserver;
import erki.talk.clients.erki.event.NickChangeEvent;
import erki.talk.clients.erki.event.OutputTextEvent;
import erki.talk.clients.erki.event.QuitEvent;
import erki.talk.clients.erki.event.TerminateEvent;
import erki.talk.clients.erki.ui.SwingUI;
/**
* This class contains the main where everything starts. It is also the main
* event collector and dispatcher. To be more specific it does not only dispatch
* the incoming events but schedule them in the order in which they arrived.
* <p>
* Other classes may register themselves as {@link EventObserver} for a special
* type of event and will subsequently be notified if such an event occurrs.
* <p>
* This class obeyes the singleton design pattern. The one and only instance is
* created by the main method via a private constructor. Other classes get
* obtain access to this instance via the {@link #getInstance()} method.
*
* @author Edgar Kalkowski
*/
public class Controller {
/** The version of this program. For use where needed. */
public static final String VERSION = "0.5.0";
private static final File SETTINGS_FILE = new File(System
.getProperty("user.home")
+ System.getProperty("file.separator")
+ ".erkitalk"
+ System.getProperty("file.separator") + "erkitalkclientrc")
.getAbsoluteFile();
/** Stores a mapping from Events to the observers of that event. */
private Map<Class<? extends Event>, Collection<EventObserver>> eventObservers = new TreeMap<Class<? extends Event>, Collection<EventObserver>>(
new Comparator<Class<? extends Event>>() {
@Override
public int compare(Class<? extends Event> o1,
Class<? extends Event> o2) {
return o1.getCanonicalName().compareTo(
o2.getCanonicalName());
}
});
/** The event queue to which all events that someone dispatches are added. */
private LinkedList<Event> eventQueue = new LinkedList<Event>();
/** This is the one and only instance of the controller. */
private static Controller instance;
private static String nick, nick2;
private static Locale locale;
private static String host = null;
private static int port;
/**
* Adhering the singleton design pattern this constructor is private and
* only called once from the main method. Initializes a user interface and
* starts the event scheduler. Other classes can obtain an instance of this
* class later by calling the {@link #getInstance()} method.
*/
private Controller() {
Controller.instance = this;
// Check on QuitEvent if active connection has to be terminated.
register(QuitEvent.class, new EventObserver<QuitEvent>() {
@Override
public void inform(QuitEvent event) {
saveSettings();
if (!Connection.isConnected()) {
dispatchEvent(new TerminateEvent());
}
};
});
// Initiate connection on ConnectEvent and store new host and port for
// later inclusion in the settings file.
register(ConnectEvent.class, new EventObserver<ConnectEvent>() {
@Override
public void inform(ConnectEvent event) {
host = event.getHost();
port = event.getPort();
Connection.makeConnection(event.getHost(), event.getPort());
}
});
// Start event scheduler first.
Logger.info(this, "Starting event scheduler.");
new Thread("EventScheduler") {
private boolean killed = false;
// Wait for a TerminateEvent and exit the dispatcher thread then.
{
register(TerminateEvent.class,
new EventObserver<TerminateEvent>() {
@Override
public void inform(TerminateEvent event) {
Logger.info(Controller.this, "Killing event "
+ "scheduler.");
killed = true;
}
});
}
/** Schedules all the events that occurr in this program. */
@Override
public void run() {
super.run();
while (!killed) {
synchronized (eventQueue) {
if (eventQueue.isEmpty()) {
try {
eventQueue.wait();
} catch (InterruptedException e) {
}
} else {
Event event = eventQueue.poll();
Logger.info(Controller.this, "Dispatching " + event
+ " (" + eventQueue.size() + " event"
+ (eventQueue.size() == 1 ? "" : "s")
+ " left).");
synchronized (eventObservers) {
if (eventObservers
.containsKey(event.getClass())
&& !eventObservers
.get(event.getClass())
.isEmpty()) {
/*
* This conversion to array prevents
* ConcurrentModificationExceptions if one
* of the events triggers the deregistration
* of an observer.
*/
EventObserver[] observers = eventObservers
.get(event.getClass()).toArray(
new EventObserver[0]);
for (EventObserver observer : observers) {
observer.inform(event);
}
} else {
Logger.warning(Controller.this, "No one "
+ "observes "
+ event.getClass().getSimpleName()
+ "s!");
dispatchEvent(new OutputTextEvent(
"No one observes "
+ event.getClass()
.getSimpleName()
+ "s!", Formatter
.getWarningFormat()));
}
}
}
}
}
Logger.info(Controller.this, "Event scheduler killed.");
}
}.start();
// Start user interface.
new SwingUI();
// Automatically connect to the last server.
if (host != null) {
dispatchEvent(new ConnectEvent(host, port));
}
}
/**
* @return the one and only instance of this class (adhering to the
* singleton design pattern).
*/
public static Controller getInstance() {
return instance;
}
/** @return The default nickname to use. */
public static String getNick() {
return nick;
}
/**
* @return The alternate nickname to use if the default one is already in
* use.
*/
public static String getAlternateNick() {
return nick2;
}
/**
* Everyone can register themself as an observer of specific events via this
* method.
*
* @param <EventType>
* The type of event one wants to be notified about.
* @param event
* Class object of the event to register for.
* @param observer
* The observer that wants to be notified if an event of this type
* occurrs.
*/
/*
* The generic type of this method ensures that only EventObservers of a
* matching type are added to the eventObservers mapping. Sadly one cannot
* force this by typing the member variable. But nevertheless this ensures
* that all the warnings in this class can be ignored.
*/
public <EventType extends Event> void register(Class<EventType> eventType,
EventObserver<EventType> observer) {
synchronized (eventObservers) {
Logger.debug(this, "Registered " + observer + ".");
if (eventObservers.containsKey(eventType)) {
eventObservers.get(eventType).add(observer);
} else {
LinkedList<EventObserver> observers = new LinkedList<EventObserver>();
observers.add(observer);
eventObservers.put(eventType, observers);
}
}
}
/**
* This method removes an observer of a specific event that was added via
* {@link #register(Class, EventObserver)} before. If no matching observer
* was found (and thus no observer could be removed) a warning is printed to
* the log.
*
* @param <EventType>
* The type of event one no longer wants to be notified about.
* @param eventType
* Class object of the event that shall be deregistered. This would
* not really be necessary, but it's more convenient and the one who
* deregisters does know this value anyway.
* @param observer
* The actual observer instance that was
* {@link #register(Class, EventObserver)}ed before.
*/
public <EventType extends Event> void deregister(
Class<EventType> eventType, EventObserver<EventType> observer) {
synchronized (eventObservers) {
if (eventObservers.containsKey(eventType)) {
if (!eventObservers.get(eventType).remove(observer)) {
Logger.warning(this, "Tried to deregister " + observer
+ " but that one was not registered!");
} else {
Logger.debug(this, "Deregistered " + observer + ".");
}
} else {
Logger.warning(this, "Tried to deregister " + observer
+ " but that one was not registered!");
}
}
}
/**
* Everyone can dispatch events via this method. Events are executed in the
* order they arrived.
*
* @param event
* The event to schedule.
*/
public void dispatchEvent(Event event) {
synchronized (eventQueue) {
Logger.debug(this, "Added " + event + " to the event queue.");
eventQueue.offer(event);
Logger.debug(this, "There "
+ (eventQueue.size() == 1 ? "is now one" : "are now "
+ eventQueue.size()) + " event"
+ (eventQueue.size() == 1 ? "" : "s") + " in the queue.");
eventQueue.notify();
}
}
/**
* Start Erki's ErkiTalk Client.
*
* @param arguments
* The command line arguments. Which arguments the program will
* accept is displayed when starting it with »--help«.
*/
public static void main(String[] arguments) {
loadSettings();
if (locale == null) {
locale = Locale.getDefault();
}
Localizor.newInstance(locale);
TreeMap<String, String> args = CommandLineParser.parse(arguments);
if (args.containsKey("--help")) {
printHelp();
return;
}
if (args.containsKey("--debug")) {
Logger.setDebug(true);
args.remove("--debug");
}
if (args.containsKey("--nick")) {
nick = args.get("--nick");
args.remove("--nick");
}
if (args.containsKey("--nick2")) {
nick2 = args.get("--nick2");
args.remove("--nick2");
}
if (args.containsKey("--server")) {
host = args.get("--server");
args.remove("--server");
}
if (args.containsKey("--port")) {
try {
port = Integer.parseInt(args.get("--port"));
} catch (NumberFormatException e) {
Logger.warning(Controller.class, "Could not parse the port "
+ "number specified on the command line!");
}
args.remove("--port");
}
if (args.containsKey("--locale")) {
locale = Localizor.parseLocale(args.get("--locale"));
args.remove("--locale");
}
if (!args.keySet().isEmpty()) {
for (String arg : args.keySet()) {
Logger.warning(Controller.class,
"Unknown command line option: " + arg + "!");
}
}
// This must be the one and only place the controller ist instanciated!
new Controller();
}
private static void printHelp() {
System.out.println("This is Erki's ErkiTalk Client v" + VERSION);
System.out.println("Usage: java Controller [OPTIONS]");
System.out.println("Supported OPTIONS:");
System.out.println(" --debug Print lots of debug "
+ "information.");
System.out.println(" --locale LOCALE Use the language LOCALE for "
+ "the user interface. LOCALE");
System.out.println(" consists of two letter "
+ "language codes like de_DE.");
System.out.println(" --nick NAME Use NAME as default nickname.");
System.out.println(" --nick2 NAME Use NAME as alternate "
+ "nickname if the default nickname is");
System.out.println(" already in use.");
System.out.println(" --port PORT Use PORT for the server to "
+ "connect to on startup.");
System.out.println(" --server HOST Automatically connect to HOST "
+ "on startup.");
System.out.println("All these options may also be specified in the "
+ "config file");
System.out.println(SETTINGS_FILE.toString() + ".");
System.out.println("However command line arguments override the "
+ "settings from the file.");
System.out.println("© 2008 by Edgar Kalkowski (eMail@edgar-"
+ "kalkowski.de)");
}
private static void loadSettings() {
try {
BufferedReader fileIn = new BufferedReader(new FileReader(
SETTINGS_FILE));
String line, country = Locale.getDefault().getCountry(), language = Locale
.getDefault().getLanguage(), variant = Locale.getDefault()
.getVariant();
while ((line = fileIn.readLine()) != null) {
if (line.startsWith("nick = ")) {
nick = line.substring("nick = ".length());
}
if (line.startsWith("nick2 = ")) {
nick2 = line.substring("nick2 = ".length());
}
if (line.startsWith("server = ")) {
host = line.substring("server = ".length());
}
if (line.startsWith("port = ")) {
port = Integer.parseInt(line.substring("port = ".length()));
}
if (line.startsWith("country = ")) {
country = line.substring("country = ".length());
}
if (line.startsWith("language = ")) {
language = line.substring("language = ".length());
}
if (line.startsWith("variant = ")) {
variant = line.substring("variant = ".length());
}
}
locale = new Locale(language, country, variant);
fileIn.close();
Logger.info(Controller.class, "Loaded settings from "
+ SETTINGS_FILE + ".");
} catch (FileNotFoundException e) {
Logger.info(Controller.class, "No settings file found at "
+ SETTINGS_FILE.toString() + ". Using default values.");
} catch (IOException e) {
Logger.warning(Controller.class, "Could not read settings file "
+ SETTINGS_FILE.toString()
+ ". Falling back to default values.");
}
}
private static void saveSettings() {
if (!SETTINGS_FILE.getParentFile().exists()
&& !SETTINGS_FILE.getParentFile().mkdirs()) {
Logger.warning(Controller.class, "Could not create settings "
+ "directory (" + SETTINGS_FILE.getParentFile()
+ ")! So the settings could not be saved.");
return;
} else if (SETTINGS_FILE.getParentFile().exists()
&& !SETTINGS_FILE.getParentFile().isDirectory()) {
Logger.warning(Controller.class, "The directory where the "
+ "settings file should be stored ("
+ SETTINGS_FILE.getParentFile() + ") is actually a file "
+ "itself! So the settings could not be saved.");
return;
}
PrintWriter fileOut;
try {
try {
fileOut = new PrintWriter(new OutputStreamWriter(
new FileOutputStream(SETTINGS_FILE), "UTF-8"));
fileOut.println("nick = " + nick);
fileOut.println("nick2 = " + nick2);
fileOut.println("server = " + host);
fileOut.println("port = " + port);
fileOut.println("country = " + locale.getCountry());
fileOut.println("language = " + locale.getLanguage());
fileOut.println("variant = " + locale.getVariant());
fileOut.close();
Logger.info(Controller.class, "Saved settings to "
+ SETTINGS_FILE + ".");
} catch (UnsupportedEncodingException e) {
Logger.warning(Controller.class, "You system seems not to "
+ "support UTF-8 so unicode characters will be messed "
+ "up in the settings file!");
fileOut = new PrintWriter(new FileOutputStream(SETTINGS_FILE));
}
} catch (FileNotFoundException e) {
Logger.warning(Controller.class, "Could not open " + SETTINGS_FILE
+ " so no settings could be saved!");
}
}
}