From 55182528cf30475351406a2964adf4b6063d0b7b Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Fri, 14 Oct 2011 14:49:29 +0100
Subject: [PATCH] Q: What does the plugin manager do? A: It manages plugins.

---
 .../api/plugins/BatchPluginCallback.java      |   2 +-
 .../sf/briar/api/plugins/PluginManager.java   |  16 +
 .../api/plugins/StreamPluginCallback.java     |   5 +-
 .../api/transport/ConnectionDispatcher.java   |  17 ++
 api/net/sf/briar/api/ui/UiCallback.java       |  25 ++
 .../sf/briar/plugins/PluginManagerImpl.java   | 289 ++++++++++++++++++
 .../net/sf/briar/plugins/PluginsModule.java   |  16 +
 components/net/sf/briar/plugins/Poller.java   |  14 +
 .../net/sf/briar/plugins/PollerImpl.java      |  78 +++++
 .../plugins/bluetooth/BluetoothPlugin.java    |   9 +-
 test/build.xml                                |   1 +
 .../briar/plugins/PluginManagerImplTest.java  |  44 +++
 12 files changed, 510 insertions(+), 6 deletions(-)
 create mode 100644 api/net/sf/briar/api/plugins/PluginManager.java
 create mode 100644 api/net/sf/briar/api/transport/ConnectionDispatcher.java
 create mode 100644 api/net/sf/briar/api/ui/UiCallback.java
 create mode 100644 components/net/sf/briar/plugins/PluginManagerImpl.java
 create mode 100644 components/net/sf/briar/plugins/PluginsModule.java
 create mode 100644 components/net/sf/briar/plugins/Poller.java
 create mode 100644 components/net/sf/briar/plugins/PollerImpl.java
 create mode 100644 test/net/sf/briar/plugins/PluginManagerImplTest.java

diff --git a/api/net/sf/briar/api/plugins/BatchPluginCallback.java b/api/net/sf/briar/api/plugins/BatchPluginCallback.java
index 8c964ed901..8aae562002 100644
--- a/api/net/sf/briar/api/plugins/BatchPluginCallback.java
+++ b/api/net/sf/briar/api/plugins/BatchPluginCallback.java
@@ -12,5 +12,5 @@ public interface BatchPluginCallback extends PluginCallback {
 
 	void readerCreated(BatchTransportReader r);
 
-	void writerCreated(ContactId contactId, BatchTransportWriter w);
+	void writerCreated(ContactId c, BatchTransportWriter w);
 }
diff --git a/api/net/sf/briar/api/plugins/PluginManager.java b/api/net/sf/briar/api/plugins/PluginManager.java
new file mode 100644
index 0000000000..379c07bb97
--- /dev/null
+++ b/api/net/sf/briar/api/plugins/PluginManager.java
@@ -0,0 +1,16 @@
+package net.sf.briar.api.plugins;
+
+public interface PluginManager {
+
+	/**
+	 * Starts all the plugins the manager knows about and returns the number of
+	 * plugins successfully started.
+	 */
+	int startPlugins();
+
+	/**
+	 * Stops all the plugins started by startPlugins() and returns the number
+	 * of plugins successfully stopped.
+	 */
+	int stopPlugins();
+}
diff --git a/api/net/sf/briar/api/plugins/StreamPluginCallback.java b/api/net/sf/briar/api/plugins/StreamPluginCallback.java
index 74bd4e11a1..adabd8e17b 100644
--- a/api/net/sf/briar/api/plugins/StreamPluginCallback.java
+++ b/api/net/sf/briar/api/plugins/StreamPluginCallback.java
@@ -9,8 +9,7 @@ import net.sf.briar.api.transport.StreamTransportConnection;
  */
 public interface StreamPluginCallback extends PluginCallback {
 
-	void incomingConnectionCreated(StreamTransportConnection c);
+	void incomingConnectionCreated(StreamTransportConnection s);
 
-	void outgoingConnectionCreated(ContactId contactId,
-			StreamTransportConnection c);
+	void outgoingConnectionCreated(ContactId c, StreamTransportConnection s);
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionDispatcher.java b/api/net/sf/briar/api/transport/ConnectionDispatcher.java
new file mode 100644
index 0000000000..9d21d65bf9
--- /dev/null
+++ b/api/net/sf/briar/api/transport/ConnectionDispatcher.java
@@ -0,0 +1,17 @@
+package net.sf.briar.api.transport;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.TransportId;
+
+public interface ConnectionDispatcher {
+
+	void dispatchReader(TransportId t, BatchTransportReader r);
+
+	void dispatchWriter(TransportId t, ContactId c,
+			BatchTransportWriter w);
+
+	void dispatchIncomingConnection(TransportId t, StreamTransportConnection s);
+
+	void dispatchOutgoingConnection(TransportId t, ContactId c,
+			StreamTransportConnection s);
+}
diff --git a/api/net/sf/briar/api/ui/UiCallback.java b/api/net/sf/briar/api/ui/UiCallback.java
new file mode 100644
index 0000000000..d241eadb37
--- /dev/null
+++ b/api/net/sf/briar/api/ui/UiCallback.java
@@ -0,0 +1,25 @@
+package net.sf.briar.api.ui;
+
+public interface UiCallback {
+
+	/**
+	 * Presents the user with a choice among two or more named options and
+	 * returns the user's response. The message may consist of a translatable
+	 * format string and arguments.
+	 * @return An index into the array of options indicating the user's choice,
+	 * or -1 if the user cancelled the choice.
+	 */
+	int showChoice(String[] options, String... message);
+
+	/**
+	 * Asks the user to confirm an action and returns the user's response. The
+	 * message may consist of a translatable format string and arguments.
+	 */
+	boolean showConfirmationMessage(String... message);
+
+	/**
+	 * Shows a message to the user. The message may consist of a translatable
+	 * format string and arguments.
+	 */
+	void showMessage(String... message);
+}
diff --git a/components/net/sf/briar/plugins/PluginManagerImpl.java b/components/net/sf/briar/plugins/PluginManagerImpl.java
new file mode 100644
index 0000000000..adb10ed2c3
--- /dev/null
+++ b/components/net/sf/briar/plugins/PluginManagerImpl.java
@@ -0,0 +1,289 @@
+package net.sf.briar.plugins;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.TransportConfig;
+import net.sf.briar.api.TransportId;
+import net.sf.briar.api.TransportProperties;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.plugins.BatchPlugin;
+import net.sf.briar.api.plugins.BatchPluginCallback;
+import net.sf.briar.api.plugins.BatchPluginFactory;
+import net.sf.briar.api.plugins.Plugin;
+import net.sf.briar.api.plugins.PluginCallback;
+import net.sf.briar.api.plugins.PluginManager;
+import net.sf.briar.api.plugins.StreamPlugin;
+import net.sf.briar.api.plugins.StreamPluginCallback;
+import net.sf.briar.api.plugins.StreamPluginFactory;
+import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.transport.BatchTransportReader;
+import net.sf.briar.api.transport.BatchTransportWriter;
+import net.sf.briar.api.transport.ConnectionDispatcher;
+import net.sf.briar.api.transport.StreamTransportConnection;
+import net.sf.briar.api.ui.UiCallback;
+
+import com.google.inject.Inject;
+
+class PluginManagerImpl implements PluginManager {
+
+	private static final Logger LOG =
+		Logger.getLogger(PluginManagerImpl.class.getName());
+
+	private static final String[] BATCH_FACTORIES = new String[] {
+		"net.sf.briar.plugins.file.RemovableDrivePluginFactory"
+	};
+
+	private static final String[] STREAM_FACTORIES = new String[] {
+		"net.sf.briar.plugins.bluetooth.BluetoothPluginFactory",
+		"net.sf.briar.plugins.socket.SimpleSocketPluginFactory"
+	};
+
+	private final Executor executor;
+	private final DatabaseComponent db;
+	private final Poller poller;
+	private final ConnectionDispatcher dispatcher;
+	private final UiCallback uiCallback;
+	private final List<BatchPlugin> batchPlugins;
+	private final List<StreamPlugin> streamPlugins;
+
+	@Inject
+	PluginManagerImpl(Executor executor, DatabaseComponent db, Poller poller,
+			ConnectionDispatcher dispatcher, UiCallback uiCallback) {
+		this.executor = executor;
+		this.db = db;
+		this.poller = poller;
+		this.dispatcher = dispatcher;
+		this.uiCallback = uiCallback;
+		batchPlugins = new ArrayList<BatchPlugin>();
+		streamPlugins = new ArrayList<StreamPlugin>();
+	}
+
+	public synchronized int startPlugins() {
+		Set<TransportId> ids = new HashSet<TransportId>();
+		// Instantiate and start the batch plugins
+		for(String s : BATCH_FACTORIES) {
+			try {
+				Class<?> c = Class.forName(s);
+				BatchPluginFactory factory =
+					(BatchPluginFactory) c.newInstance();
+				BatchCallback callback = new BatchCallback();
+				BatchPlugin plugin = factory.createPlugin(executor, callback);
+				if(plugin == null) {
+					if(LOG.isLoggable(Level.INFO))
+						LOG.info(factory.getClass().getSimpleName() +
+						" did not create a plugin");
+					continue;
+				}
+				TransportId id = plugin.getId();
+				if(!ids.add(id)) {
+					if(LOG.isLoggable(Level.WARNING))
+						LOG.warning("Duplicate transport ID: " + id);
+					continue;
+				}
+				callback.setId(id);
+				plugin.start();
+				batchPlugins.add(plugin);
+			} catch(ClassCastException e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+				continue;
+			} catch(Exception e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+				continue;
+			}
+		}
+		// Instantiate and start the stream plugins
+		for(String s : STREAM_FACTORIES) {
+			try {
+				Class<?> c = Class.forName(s);
+				StreamPluginFactory factory =
+					(StreamPluginFactory) c.newInstance();
+				StreamCallback callback = new StreamCallback();
+				StreamPlugin plugin = factory.createPlugin(executor, callback);
+				if(plugin == null) {
+					if(LOG.isLoggable(Level.INFO))
+						LOG.info(factory.getClass().getSimpleName() +
+						" did not create a plugin");
+					continue;
+				}
+				TransportId id = plugin.getId();
+				if(!ids.add(id)) {
+					if(LOG.isLoggable(Level.WARNING))
+						LOG.warning("Duplicate transport ID: " + id);
+					continue;
+				}
+				callback.setId(id);
+				plugin.start();
+				streamPlugins.add(plugin);
+			} catch(ClassCastException e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+				continue;
+			} catch(Exception e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+				continue;
+			}
+		}
+		// Start the poller
+		List<Plugin> plugins = new ArrayList<Plugin>();
+		plugins.addAll(batchPlugins);
+		plugins.addAll(streamPlugins);
+		poller.startPolling(plugins);
+		// Return the number of plugins started
+		return batchPlugins.size() + streamPlugins.size();
+	}
+
+	public synchronized int stopPlugins() {
+		int stopped = 0;
+		// Stop the batch plugins
+		for(BatchPlugin plugin : batchPlugins) {
+			try {
+				plugin.stop();
+				stopped++;
+			} catch(IOException e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+			}
+		}
+		batchPlugins.clear();
+		// Stop the stream plugins
+		for(StreamPlugin plugin : streamPlugins) {
+			try {
+				plugin.stop();
+				stopped++;
+			} catch(IOException e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+			}
+		}
+		streamPlugins.clear();
+		// Return the number of plugins stopped
+		return stopped;
+	}
+
+	private abstract class PluginCallbackImpl implements PluginCallback {
+
+		protected volatile TransportId id = null;
+
+		protected void setId(TransportId id) {
+			assert this.id == null;
+			this.id = id;
+		}
+
+		public TransportConfig getConfig() {
+			assert id != null;
+			try {
+				return db.getConfig(id);
+			} catch(DbException e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+				return new TransportConfig();
+			}
+		}
+
+		public TransportProperties getLocalProperties() {
+			assert id != null;
+			try {
+				TransportProperties p = db.getLocalTransports().get(id);
+				return p == null ? new TransportProperties() : p;
+			} catch(DbException e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+				return new TransportProperties();
+			}
+		}
+
+		public Map<ContactId, TransportProperties> getRemoteProperties() {
+			assert id != null;
+			try {
+				return db.getRemoteProperties(id);
+			} catch(DbException e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+				return Collections.emptyMap();
+			}
+		}
+
+		public void setConfig(TransportConfig c) {
+			assert id != null;
+			try {
+				db.setConfig(id, c);
+			} catch(DbException e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+			}
+		}
+
+		public void setLocalProperties(TransportProperties p) {
+			assert id != null;
+			if(p.size() > TransportUpdate.MAX_PROPERTIES_PER_PLUGIN) {
+				if(LOG.isLoggable(Level.WARNING))
+					LOG.warning("Plugin " + id + " set too many properties");
+				return;
+			}
+			for(String s : p.keySet()) {
+				if(s.length() > TransportUpdate.MAX_KEY_OR_VALUE_LENGTH) {
+					if(LOG.isLoggable(Level.WARNING))
+						LOG.warning("Plugin " + id + " set long key: " + s);
+					return;
+				}
+			}
+			for(String s : p.values()) {
+				if(s.length() > TransportUpdate.MAX_KEY_OR_VALUE_LENGTH) {
+					if(LOG.isLoggable(Level.WARNING))
+						LOG.warning("Plugin " + id + " set long value: " + s);
+					return;
+				}
+			}
+			try {
+				db.setLocalProperties(id, p);
+			} catch(DbException e) {
+				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
+			}
+		}
+
+		public int showChoice(String[] options, String... message) {
+			return uiCallback.showChoice(options, message);
+		}
+
+		public boolean showConfirmationMessage(String... message) {
+			return uiCallback.showConfirmationMessage(message);
+		}
+
+		public void showMessage(String... message) {
+			uiCallback.showMessage(message);
+		}
+	}
+
+	private class BatchCallback extends PluginCallbackImpl
+	implements BatchPluginCallback {
+
+		public void readerCreated(BatchTransportReader r) {
+			assert id != null;
+			dispatcher.dispatchReader(id, r);
+		}
+
+		public void writerCreated(ContactId c, BatchTransportWriter w) {
+			assert id != null;
+			dispatcher.dispatchWriter(id, c, w);
+		}
+	}
+
+	private class StreamCallback extends PluginCallbackImpl
+	implements StreamPluginCallback {
+
+		public void incomingConnectionCreated(StreamTransportConnection s) {
+			assert id != null;
+			dispatcher.dispatchIncomingConnection(id, s);
+		}
+
+		public void outgoingConnectionCreated(ContactId c,
+				StreamTransportConnection s) {
+			assert id != null;
+			dispatcher.dispatchOutgoingConnection(id, c, s);
+		}
+	}
+}
\ No newline at end of file
diff --git a/components/net/sf/briar/plugins/PluginsModule.java b/components/net/sf/briar/plugins/PluginsModule.java
new file mode 100644
index 0000000000..5cb124da21
--- /dev/null
+++ b/components/net/sf/briar/plugins/PluginsModule.java
@@ -0,0 +1,16 @@
+package net.sf.briar.plugins;
+
+import net.sf.briar.api.plugins.PluginManager;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Singleton;
+
+public class PluginsModule extends AbstractModule {
+
+	@Override
+	protected void configure() {
+		bind(PluginManager.class).to(
+				PluginManagerImpl.class).in(Singleton.class);
+		bind(Poller.class).to(PollerImpl.class);
+	}
+}
diff --git a/components/net/sf/briar/plugins/Poller.java b/components/net/sf/briar/plugins/Poller.java
new file mode 100644
index 0000000000..0c349722aa
--- /dev/null
+++ b/components/net/sf/briar/plugins/Poller.java
@@ -0,0 +1,14 @@
+package net.sf.briar.plugins;
+
+import java.util.Collection;
+
+import net.sf.briar.api.plugins.Plugin;
+
+interface Poller {
+
+	/** Starts a new thread to poll the given collection of plugins. */
+	void startPolling(Collection<Plugin> plugins);
+
+	/** Tells the poller thread to exit. */
+	void stopPolling();
+}
diff --git a/components/net/sf/briar/plugins/PollerImpl.java b/components/net/sf/briar/plugins/PollerImpl.java
new file mode 100644
index 0000000000..5905fd243f
--- /dev/null
+++ b/components/net/sf/briar/plugins/PollerImpl.java
@@ -0,0 +1,78 @@
+package net.sf.briar.plugins;
+
+import java.util.Collection;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.sf.briar.api.plugins.Plugin;
+
+class PollerImpl implements Poller, Runnable {
+
+	private static final Logger LOG =
+		Logger.getLogger(PollerImpl.class.getName());
+
+	private final SortedSet<PollTime> pollTimes = new TreeSet<PollTime>();
+
+	public synchronized void startPolling(Collection<Plugin> plugins) {
+		for(Plugin plugin : plugins) schedule(plugin);
+		new Thread(this).start();
+	}
+
+	private synchronized void schedule(Plugin plugin) {
+		if(plugin.shouldPoll()) {
+			long now = System.currentTimeMillis();
+			long interval = plugin.getPollingInterval();
+			pollTimes.add(new PollTime(now + interval, plugin));
+		}
+	}
+
+	public synchronized void stopPolling() {
+		pollTimes.clear();
+		notifyAll();
+	}
+
+	public void run() {
+		while(true) {
+			synchronized(this) {
+				if(pollTimes.isEmpty()) return;
+				PollTime p = pollTimes.first();
+				long now = System.currentTimeMillis();
+				if(now <= p.time) {
+					pollTimes.remove(p);
+					try {
+						p.plugin.poll();
+					} catch(RuntimeException e) {
+						if(LOG.isLoggable(Level.WARNING))
+							LOG.warning("Plugin " + p.plugin.getId() + " " + e);
+					}
+					schedule(p.plugin);
+				} else {
+					try {
+						wait(p.time - now);
+					} catch(InterruptedException ignored) {}
+				}
+			}
+		}
+	}
+
+	private static class PollTime implements Comparable<PollTime> {
+
+		private final long time;
+		private final Plugin plugin;
+
+		private PollTime(long time, Plugin plugin) {
+			this.time = time;
+			this.plugin = plugin;
+		}
+
+		public int compareTo(PollTime p) {
+			if(time < p.time) return -1;
+			if(time > p.time) return 1;
+			if(plugin.getId().getInt() < p.plugin.getId().getInt()) return -1;
+			if(plugin.getId().getInt() > p.plugin.getId().getInt()) return 1;
+			return 0;
+		}
+	}
+}
diff --git a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
index b906dc073d..d5db89a720 100644
--- a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
+++ b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
@@ -63,8 +63,13 @@ class BluetoothPlugin extends AbstractPlugin implements StreamPlugin {
 			}
 		} catch(UnsatisfiedLinkError e) {
 			// On Linux the user may need to install libbluetooth-dev
-			if(OsUtils.isLinux())
-				callback.showMessage("BLUETOOTH_INSTALL LIBS");
+			if(OsUtils.isLinux()) {
+				executor.execute(new Runnable() {
+					public void run() {
+						callback.showMessage("BLUETOOTH_INSTALL_LIBS");
+					}
+				});
+			}
 			throw new IOException(e.getMessage());
 		}
 		executor.execute(createBinder());
diff --git a/test/build.xml b/test/build.xml
index b5e2813b04..b0d0118ed9 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -26,6 +26,7 @@
 			<test name='net.sf.briar.i18n.FontManagerTest'/>
 			<test name='net.sf.briar.i18n.I18nTest'/>
 			<test name='net.sf.briar.invitation.InvitationWorkerTest'/>
+			<test name='net.sf.briar.plugins.PluginManagerImplTest'/>
 			<test name='net.sf.briar.plugins.file.LinuxRemovableDriveFinderTest'/>
 			<test name='net.sf.briar.plugins.file.MacRemovableDriveFinderTest'/>
 			<test name='net.sf.briar.plugins.file.PollingRemovableDriveMonitorTest'/>
diff --git a/test/net/sf/briar/plugins/PluginManagerImplTest.java b/test/net/sf/briar/plugins/PluginManagerImplTest.java
new file mode 100644
index 0000000000..f8260fa81a
--- /dev/null
+++ b/test/net/sf/briar/plugins/PluginManagerImplTest.java
@@ -0,0 +1,44 @@
+package net.sf.briar.plugins;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+import junit.framework.TestCase;
+import net.sf.briar.api.TransportId;
+import net.sf.briar.api.TransportProperties;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.transport.ConnectionDispatcher;
+import net.sf.briar.api.ui.UiCallback;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.junit.Test;
+
+public class PluginManagerImplTest extends TestCase {
+
+	@Test
+	public void testStartAndStop() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Map<?, ?> localTransports = context.mock(Map.class);
+		final ConnectionDispatcher dispatcher =
+			context.mock(ConnectionDispatcher.class);
+		final UiCallback uiCallback = context.mock(UiCallback.class);
+		context.checking(new Expectations() {{
+			allowing(db).getLocalTransports();
+			will(returnValue(localTransports));
+			allowing(localTransports).get(with(any(TransportId.class)));
+			will(returnValue(new TransportProperties()));
+			allowing(db).getRemoteProperties(with(any(TransportId.class)));
+			will(returnValue(new TransportProperties()));
+		}});
+		Executor executor = new ImmediateExecutor();
+		Poller poller = new PollerImpl();
+		PluginManagerImpl p =
+			new PluginManagerImpl(executor, db, poller, dispatcher, uiCallback);
+		// The Bluetooth plugin will not start without a Bluetooth device, so
+		// we expect two plugins to be started
+		assertEquals(2, p.startPlugins());
+		assertEquals(2, p.stopPlugins());
+	}
+}
-- 
GitLab