[freenet-cvs] r15624 - in trunk/freenet/src/freenet: clients/http l10n node pluginmanager support support/io

bombe at freenetproject.org bombe at freenetproject.org
Sun Oct 28 18:44:09 UTC 2007


Author: bombe
Date: 2007-10-28 18:44:08 +0000 (Sun, 28 Oct 2007)
New Revision: 15624

Added:
   trunk/freenet/src/freenet/support/JarClassLoader.java
   trunk/freenet/src/freenet/support/io/Closer.java
   trunk/freenet/src/freenet/support/io/StreamCopier.java
Modified:
   trunk/freenet/src/freenet/clients/http/PproxyToadlet.java
   trunk/freenet/src/freenet/l10n/freenet.l10n.en.properties
   trunk/freenet/src/freenet/node/TextModeClientInterface.java
   trunk/freenet/src/freenet/pluginmanager/PluginHandler.java
   trunk/freenet/src/freenet/pluginmanager/PluginInfoWrapper.java
   trunk/freenet/src/freenet/pluginmanager/PluginManager.java
Log:
fix #1815, start plugins in background thread
improve plugin loading interface
save downloaded plugins

Modified: trunk/freenet/src/freenet/clients/http/PproxyToadlet.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/PproxyToadlet.java	2007-10-28 18:33:01 UTC (rev 15623)
+++ trunk/freenet/src/freenet/clients/http/PproxyToadlet.java	2007-10-28 18:44:08 UTC (rev 15624)
@@ -9,6 +9,7 @@
 import java.net.URL;
 import java.util.Date;
 import java.util.Iterator;
+import java.util.Set;
 
 import freenet.client.HighLevelSimpleClient;
 import freenet.l10n.L10n;
@@ -21,9 +22,11 @@
 import freenet.pluginmanager.PluginInfoWrapper;
 import freenet.pluginmanager.PluginManager;
 import freenet.pluginmanager.RedirectPluginHTTPException;
+import freenet.pluginmanager.PluginManager.PluginProgress;
 import freenet.support.HTMLNode;
 import freenet.support.Logger;
 import freenet.support.MultiValueTable;
+import freenet.support.TimeUtil;
 import freenet.support.api.HTTPRequest;
 import freenet.support.io.FileUtil;
 
@@ -118,104 +121,28 @@
 		}
 		else
 		{
+			final boolean logMINOR = Logger.shouldLog(Logger.MINOR, this);
+			final boolean logNORMAL = Logger.shouldLog(Logger.NORMAL, this);
 
-			if (request.isPartSet("load")) {
-				String filename = request.getPartAsString("load", MAX_PLUGIN_NAME_LENGTH);
-				final boolean logMINOR = Logger.shouldLog(Logger.MINOR, this);
-				boolean downloaded = false;
-
-				if(logMINOR) Logger.minor(this, "Loading "+filename);
-				if (filename.endsWith("#")) {
-					for (int tries = 0; (tries <= 5) && (downloaded == false); tries++) {
-						if (filename.indexOf('@') > -1) {
-							Logger
-							.error(this,
-							"We don't allow downloads from anywhere else but our server");
-							sendErrorPage(ctx, 403, l10n("Error"), l10n("downloadNotAllowedFromRemoteServer"));
-							return;
-						}
-						String pluginname = filename.substring(0,
-								filename.length() - 1);
-						filename = null;
-
-						URL url;
-						InputStream is = null;
-
-						try {
-							url = new URL(
-									"http://downloads.freenetproject.org/alpha/plugins/"
-									+ pluginname + ".jar.url");
-							if (logMINOR)
-								Logger.minor(this, "Downloading " + url);
-							is = url.openStream();
-
-							File pluginsDirectory = new File("plugins");
-							if (!pluginsDirectory.exists()) {
-								Logger
-								.normal(this,
-								"The plugin directory hasn't been found, let's create it");
-								if (!pluginsDirectory.mkdir()) {
-									sendErrorPage(ctx, 500, l10n("Error"), l10n("pluginDirectoryNotCreated"));
-									return;
-								}
-							}
-
-							File finalFile = new File("plugins/" + pluginname
-									+ ".jar");
-							if (!FileUtil.writeTo(is, finalFile))
-								Logger.error(this,
-										"Failed to rename the temporary file into "
-										+ finalFile);
-
-							filename = "*@file://"
-								+ FileUtil.getCanonicalFile(finalFile);
-							if (logMINOR)
-								Logger.minor(this, "Rewritten to " + filename);
-							downloaded = true;
-						} catch (MalformedURLException mue) {
-							Logger.error(this,
-									"MalformedURLException has occured : " + mue,
-									mue);
-							sendErrorPage(ctx, l10n("Error"), l10n("pluginNotDownloaded"), mue);
-							return;
-						} catch (FileNotFoundException e) {
-							Logger.error(this,
-									"FileNotFoundException has occured : " + e, e);
-							sendErrorPage(ctx, l10n("Error"), l10n("pluginNotDownloaded"), e);
-							return;
-						} catch (IOException ioe) {
-							System.out.println("Caught :" + ioe.getMessage());
-							ioe.printStackTrace();
-							sendErrorPage(ctx, l10n("Error"), l10n("pluginNotDownloaded"), ioe);
-							return;
-						} finally {
-							try {
-								if (is != null)
-									is.close();
-							} catch (IOException ioe) {
-							}
-						}
-					}
-					if (filename == null) {
-						sendErrorPage(ctx, 500, l10n("Error"), l10n("pluginNotDownloaded"));
-						return;
-					}
-					else if(!downloaded) {
-						Logger.error(this, "Can't load the given plugin; giving up");
-						sendErrorPage(ctx, 500, l10n("Error"), l10n("pluginNotDownloaded"));
-						return;
-					}
+			if (request.isPartSet("submit-official") || request.isPartSet("submit-other")) {
+				String pluginName = null;
+				boolean refresh = request.isPartSet("refresh-on-startup");
+				if (request.isPartSet("submit-official")) {
+					pluginName = request.getPartAsString("plugin-name", 40);
+				} else {
+					pluginName = request.getPartAsString("plugin-url", 200);
 				}
-
-				pm.startPlugin(filename, true);
+				pm.startPlugin(pluginName, refresh, true);
 				headers.put("Location", ".");
 				ctx.sendReplyHeaders(302, "Found", headers, null, 0);
 				return;
-			}if (request.isPartSet("cancel")){
+			}
+			if (request.isPartSet("cancel")){
 				headers.put("Location", "/plugins/");
 				ctx.sendReplyHeaders(302, "Found", headers, null, 0);
 				return;
-			}if (request.getPartAsString("unloadconfirm", MAX_PLUGIN_NAME_LENGTH).length() > 0) {
+			}
+			if (request.getPartAsString("unloadconfirm", MAX_PLUGIN_NAME_LENGTH).length() > 0) {
 				pm.killPlugin(request.getPartAsString("unloadconfirm", MAX_PLUGIN_NAME_LENGTH), MAX_THREADED_UNLOAD_WAIT_TIME);
 				HTMLNode pageNode = ctx.getPageMaker().getPageNode(l10n("plugins"), ctx);
 				HTMLNode contentNode = ctx.getPageMaker().getContentNode(pageNode);
@@ -243,11 +170,13 @@
 				return;
 			}else if (request.getPartAsString("reload", MAX_PLUGIN_NAME_LENGTH).length() > 0) {
 				String fn = null;
+				boolean refresh = false;
 				Iterator it = pm.getPlugins().iterator();
 				while (it.hasNext()) {
 					PluginInfoWrapper pi = (PluginInfoWrapper) it.next();
 					if (pi.getThreadName().equals(request.getPartAsString("reload", MAX_PLUGIN_NAME_LENGTH))) {
 						fn = pi.getFilename();
+						refresh = pi.isAutoRefresh();
 						break;
 					}
 				}
@@ -257,7 +186,7 @@
 							L10n.getString("PluginToadlet.pluginNotFoundReload"));
 				} else {
 					pm.killPlugin(request.getPartAsString("reload", MAX_PLUGIN_NAME_LENGTH), MAX_THREADED_UNLOAD_WAIT_TIME);
-					pm.startPlugin(fn, true);
+					pm.startPlugin(fn, refresh, true);
 
 					headers.put("Location", ".");
 					ctx.sendReplyHeaders(302, "Found", headers, null, 0);
@@ -297,7 +226,18 @@
 			Logger.minor(this, "Pproxy fetching "+path);
 		try {
 			if (path.equals("")) {
-				this.showPluginList(ctx, request, pm);
+				if (!ctx.isAllowedFullAccess()) {
+					super.sendErrorPage(ctx, 403, "Unauthorized", L10n.getString("Toadlet.unauthorized"));
+					return;
+				}
+
+				HTMLNode pageNode = ctx.getPageMaker().getPageNode(l10n("pluginsWithNodeName", "name", core.getMyName()), ctx);
+				HTMLNode contentNode = ctx.getPageMaker().getContentNode(pageNode);
+
+				this.showStartingPlugins(ctx, request, pm, contentNode);
+				this.showPluginList(ctx, request, pm, contentNode);
+
+				writeHTMLReply(ctx, 200, "OK", pageNode.generate());
 			} else {
 				// split path into plugin class name and 'data' path for plugin
 				int to = path.indexOf("/");
@@ -336,16 +276,44 @@
 		}
 	}
 
-	private void showPluginList(ToadletContext ctx, HTTPRequest request, PluginManager pm) throws ToadletContextClosedException, IOException {
-		if(!ctx.isAllowedFullAccess()) {
-			super.sendErrorPage(ctx, 403, "Unauthorized", L10n.getString("Toadlet.unauthorized"));
-			return;
+	/**
+	 * Shows a list of all currently loading plugins.
+	 * 
+	 * @param toadletContext
+	 *            The toadlet context
+	 * @param request
+	 *            The HTTP request
+	 * @param pluginManager
+	 *            The plugin manager
+	 * @throws ToadletContextClosedException
+	 *             if the toadlet context is closed
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	private void showStartingPlugins(ToadletContext toadletContext, HTTPRequest request, PluginManager pluginManager, HTMLNode contentNode) throws ToadletContextClosedException, IOException {
+		Set/*<PluginProgress>*/ startingPlugins = pluginManager.getStartingPlugins();
+		if (!startingPlugins.isEmpty()) {
+			HTMLNode startingPluginsBox = contentNode.addChild("div", "class", "infobox infobox-normal");
+			startingPluginsBox.addChild("div", "class", "infobox-header", l10n("startingPluginsTitle"));
+			HTMLNode startingPluginsContent = startingPluginsBox.addChild("div", "class", "infobox-content");
+			HTMLNode startingPluginsTable = startingPluginsContent.addChild("table");
+			HTMLNode startingPluginsHeader = startingPluginsTable.addChild("tr");
+			startingPluginsHeader.addChild("th", l10n("startingPluginName"));
+			startingPluginsHeader.addChild("th", l10n("startingPluginStatus"));
+			startingPluginsHeader.addChild("th", l10n("startingPluginTime"));
+			Iterator/*<PluginProgress>*/ startingPluginsIterator = startingPlugins.iterator();
+			while (startingPluginsIterator.hasNext()) {
+				PluginProgress pluginProgress = (PluginProgress) startingPluginsIterator.next();
+				HTMLNode startingPluginsRow = startingPluginsTable.addChild("tr");
+				startingPluginsRow.addChild("td", pluginProgress.getName());
+				startingPluginsRow.addChild("td", l10n("startingPluginStatus." + pluginProgress.getProgress().toString()));
+				startingPluginsRow.addChild("td", "aligh", "right", TimeUtil.formatTime(pluginProgress.getTime()));
+			}
 		}
+	}
 
+	private void showPluginList(ToadletContext ctx, HTTPRequest request, PluginManager pm, HTMLNode contentNode) throws ToadletContextClosedException, IOException {
 		if (!request.hasParameters()) {
-			HTMLNode pageNode = ctx.getPageMaker().getPageNode(l10n("pluginsWithNodeName", "name", core.getMyName()), ctx);
-			HTMLNode contentNode = ctx.getPageMaker().getContentNode(pageNode);
-
 			HTMLNode infobox = contentNode.addChild("div", "class", "infobox infobox-normal");
 			infobox.addChild("div", "class", "infobox-header", L10n.getString("PluginToadlet.pluginListTitle"));
 			HTMLNode infoboxContent = infobox.addChild("div", "class", "infobox-content");
@@ -386,12 +354,27 @@
 				}
 			}
 
-			HTMLNode addForm = ctx.addFormChild(infoboxContent, ".", "addPluginForm");
-			HTMLNode loadDiv = addForm.addChild("div");
-			loadDiv.addChild("#", (l10n("loadPluginLabel") + ' '));
-			loadDiv.addChild("input", new String[] { "type", "name", "size" }, new String[] { "text", "load", "40" });
-			loadDiv.addChild("input", new String[] { "type", "value" }, new String[] { "submit", l10n("Load") });
-			writeHTMLReply(ctx, 200, "OK", pageNode.generate());
+			/* box for "official" plugins. */
+			HTMLNode addOfficialPluginBox = contentNode.addChild("div", "class", "infobox infobox-normal");
+			addOfficialPluginBox.addChild("div", "class", "infobox-header", l10n("loadOfficialPlugin"));
+			HTMLNode addOfficialPluginContent = addOfficialPluginBox.addChild("div", "class", "infobox-content");
+			HTMLNode addOfficialForm = ctx.addFormChild(addOfficialPluginContent, ".", "addOfficialPluginForm");
+			addOfficialForm.addChild("div", l10n("loadOfficialPluginText"));
+			addOfficialForm.addChild("#", (l10n("loadOfficialPluginLabel") + ": "));
+			addOfficialForm.addChild("input", new String[] { "type", "name", "size" }, new String[] { "text", "plugin-name", "40" });
+			addOfficialForm.addChild("input", new String[] { "type", "name", "value", "checked" }, new String[] { "checkbox", "refresh-on-startup", "tue", "checked" }, l10n("refreshOnStartup"));
+			addOfficialForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "submit-official", l10n("Load") });
+
+			/* box for unofficial plugins. */
+			HTMLNode addOtherPluginBox = contentNode.addChild("div", "class", "infobox infobox-normal");
+			addOtherPluginBox.addChild("div", "class", "infobox-header", l10n("loadOtherPlugin"));
+			HTMLNode addOtherPluginContent = addOtherPluginBox.addChild("div", "class", "infobox-content");
+			HTMLNode addOtherForm = ctx.addFormChild(addOtherPluginContent, ".", "addOtherPluginForm");
+			addOtherForm.addChild("div", l10n("loadOtherPluginText"));
+			addOtherForm.addChild("#", (l10n("loadOtherURLLabel") + ": "));
+			addOtherForm.addChild("input", new String[] { "type", "name", "size" }, new String[] { "text", "plugin-url", "80" });
+			addOtherForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "checkbox", "refresh-on-startup", "true" }, l10n("refreshOnStartup"));
+			addOtherForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "submit-other", l10n("Load") });
 		} 
 	}
 }

Modified: trunk/freenet/src/freenet/l10n/freenet.l10n.en.properties
===================================================================
--- trunk/freenet/src/freenet/l10n/freenet.l10n.en.properties	2007-10-28 18:33:01 UTC (rev 15623)
+++ trunk/freenet/src/freenet/l10n/freenet.l10n.en.properties	2007-10-28 18:44:08 UTC (rev 15624)
@@ -684,6 +684,9 @@
 PluginManager.loadedPluginsLong=A list of plugins that are started when the node starts
 PluginManager.pluginReqNewerJVM=The plugin ${name} seems to require a later JVM. Please install at least Sun java 1.5, or remove the plugin.
 PluginManager.pluginReqNewerJVMTitle=Later JVM required by plugin ${name}.
+PluginManager.pluginLoadingFailedTitle=Could not load plugin!
+PluginManager.pluginLoadingFailed=The plugin specified by ${name} could not be loaded
+PluginManager.pluginLoadingFailedWithMessage=The plugin specified by ${name} could not be loaded: ${message}
 PluginToadlet.addPluginTitle=Add a plugin
 PluginToadlet.failedToLoadPlugin=Failed to load plugin.
 PluginToadlet.failedToLoadPluginCheckClass=The plugin you requested could not be loaded. Please verify the name of the plugin's class and the URL, if you gave one.
@@ -706,7 +709,12 @@
 PproxyToadlet.Error=Error
 PproxyToadlet.internalIDTitle=Internal ID
 PproxyToadlet.Load=Load
-PproxyToadlet.loadPluginLabel=Load Plugin:
+PproxyToadlet.loadOfficialPlugin=Add an Official Plugin
+PproxyToadlet.loadOfficialPluginText=These plugins are hosted on servers of The Freenet Project. We believe that these plugins are free of privacy leaks though we will not guarantee it. Possible plugin names are: Echo, Freemail, HelloWorld (yes, really), JSTUN, Librarian, MDNSDiscovery, SNMP, TestGallery, UPnP, XMLLibrarian, XMLSpider. Plugin names are case-sensitive.
+PproxyToadlet.loadOfficialPluginLabel=Load Official Plugin
+PproxyToadlet.loadOtherPlugin=Add an Unofficial Plugin
+PproxyToadlet.loadOtherPluginText=Here you can enter the URL of a plugin you want to load. Other plugins than the ones listed above are not even remotely supported or checked for privacy leaks by us, so if you load a remote plugin here, you are basically on your own.
+PproxyToadlet.loadOtherURLLabel=Plugin URL
 PproxyToadlet.noPlugins=No plugins loaded
 PproxyToadlet.pluginNotFoundReload=The specified plugin could not be located in order to reload it.
 PproxyToadlet.pluginNotFoundReloadTitle=Plugin Not Found (reloading)
@@ -714,9 +722,16 @@
 PproxyToadlet.pluginUnloadedWithName=The plugin ${name} has been unloaded.
 PproxyToadlet.plugins=Plugins
 PproxyToadlet.pluginsWithNodeName=Plugins of ${name}
+PproxyToadlet.refreshOnStartup=Reload from server on startup
 PproxyToadlet.reload=Reload
 PproxyToadlet.returnToPluginPage=Return to plugin page
 PproxyToadlet.startedAtTitle=Started at
+PproxyToadlet.startingPluginsTitle=Starting Plugins
+PproxyToadlet.startingPluginName=Plugin name
+PproxyToadlet.startingPluginStatus=Current status
+PproxyToadlet.startingPluginStatus.downloading=downloading
+PproxyToadlet.startingPluginStatus.starting=starting
+PproxyToadlet.startingPluginTime=Time spent
 PproxyToadlet.pluginDirectoryNotCreated=The plugin directory could not be created.
 PproxyToadlet.pluginNotDownloaded=The plugin could not be downloaded.
 PproxyToadlet.pluginStopping=Plugin Stopping

Modified: trunk/freenet/src/freenet/node/TextModeClientInterface.java
===================================================================
--- trunk/freenet/src/freenet/node/TextModeClientInterface.java	2007-10-28 18:33:01 UTC (rev 15623)
+++ trunk/freenet/src/freenet/node/TextModeClientInterface.java	2007-10-28 18:44:08 UTC (rev 15624)
@@ -878,22 +878,19 @@
         	probeAll();
         } else if(uline.startsWith("PLUGLOAD:")) {
         	if (line.substring("PLUGLOAD:".length()).trim().equals("?")) {
-        		outsb.append("  PLUGLOAD: pkg.Class                  - Load plugin from current classpath");        		
-        		outsb.append("  PLUGLOAD: pkg.Class at file:<filename>  - Load plugin from file");
-        		outsb.append("  PLUGLOAD: pkg.Class at http://...       - Load plugin from online file");
-        		outsb.append("  PLUGLOAD:         *@...              - Load plugin from manifest in given jarfile");
+        		outsb.append("  PLUGLOAD: pluginName         - Load official plugin from freenetproject.org");
+        		outsb.append("  PLUGLOAD: file://<filename>  - Load plugin from file");
+        		outsb.append("  PLUGLOAD: http://...         - Load plugin from online file");
         		outsb.append("");
-        		outsb.append("If the filename/url ends with \".url\", it" +
-        				" is treated as a link, meaning that the first line is" +
-        				" the accual URL. Else it is loaded as classpath and" +
-        				" the class it loaded from it (meaning the file could" +
-        				" be either a jar-file or a class-file).");
-        		outsb.append("");
-        		outsb.append("  PLUGLOAD: pkg.Class*  - Load newest version of plugin from http://downloads.freenetproject.org/alpha/plugins/");        		
-        		outsb.append("");
-        		
-        	} else
-        		n.pluginManager.startPlugin(line.substring("PLUGLOAD:".length()).trim(), true);
+        		outsb.append("If you append as asterisk (\"*\") to the name or URL, the plugin will be reloaded from the remote server on startup.");
+        	} else {
+        		String name = line.substring("PLUGLOAD:".length()).trim();
+        		boolean refresh = name.endsWith("*");
+        		if (refresh) {
+        			name = name.substring(0, name.length() - 1);
+        		}
+        		n.pluginManager.startPlugin(name, refresh, true);
+        	}
             //outsb.append("PLUGLOAD: <pkg.classname>[(@<URI to jarfile.jar>|<<URI to file containing real URI>|* (will load from freenets pluginpool))] - Load plugin.");
         } else if(uline.startsWith("PLUGLIST")) {
         	outsb.append(n.pluginManager.dumpPlugins());

Modified: trunk/freenet/src/freenet/pluginmanager/PluginHandler.java
===================================================================
--- trunk/freenet/src/freenet/pluginmanager/PluginHandler.java	2007-10-28 18:33:01 UTC (rev 15623)
+++ trunk/freenet/src/freenet/pluginmanager/PluginHandler.java	2007-10-28 18:44:08 UTC (rev 15624)
@@ -19,8 +19,8 @@
 	 * 
 	 * @param plug
 	 */
-	public static PluginInfoWrapper startPlugin(PluginManager pm, String filename, FredPlugin plug, PluginRespirator pr) {
-		final PluginInfoWrapper pi = new PluginInfoWrapper(plug, filename);
+	public static PluginInfoWrapper startPlugin(PluginManager pm, String filename, FredPlugin plug, PluginRespirator pr, boolean refresh) {
+		final PluginInfoWrapper pi = new PluginInfoWrapper(plug, filename, refresh);
 		final PluginStarter ps = new PluginStarter(pr, pi);
 		ps.setPlugin(pm, plug);
 		// We must start the plugin *after startup has finished*

Modified: trunk/freenet/src/freenet/pluginmanager/PluginInfoWrapper.java
===================================================================
--- trunk/freenet/src/freenet/pluginmanager/PluginInfoWrapper.java	2007-10-28 18:33:01 UTC (rev 15623)
+++ trunk/freenet/src/freenet/pluginmanager/PluginInfoWrapper.java	2007-10-28 18:44:08 UTC (rev 15624)
@@ -19,13 +19,14 @@
 	private boolean isThreadlessPlugin;
 	private boolean isIPDetectorPlugin;
 	private boolean isPortForwardPlugin;
+	private boolean autoRefresh;
 	private String filename;
 	private HashSet toadletLinks=new HashSet();
 	private boolean stopping = false;
 	private boolean unregistered = false;
 	//public String 
 	
-	public PluginInfoWrapper(FredPlugin plug, String filename) {
+	public PluginInfoWrapper(FredPlugin plug, String filename, boolean autoRefresh) {
 		this.plug = plug;
 		if (fedPluginThread) return;
 		className = plug.getClass().toString();
@@ -37,8 +38,20 @@
 		isThreadlessPlugin = (plug instanceof FredPluginThreadless);
 		isIPDetectorPlugin = (plug instanceof FredPluginIPDetector);
 		isPortForwardPlugin = (plug instanceof FredPluginPortForward);
+		this.autoRefresh = autoRefresh;
 	}
-	
+
+	/**
+	 * Returns whether this plugin should be refreshed from the server on
+	 * startup.
+	 * 
+	 * @return <code>true</code> if the plugin should be refresh on startup,
+	 *         <code>false</code> otherwise
+	 */
+	public boolean isAutoRefresh() {
+		return autoRefresh;
+	}
+
 	void setThread(Thread ps) {
 		if(thread != null)
 			throw new IllegalStateException("Already set a thread");

Modified: trunk/freenet/src/freenet/pluginmanager/PluginManager.java
===================================================================
--- trunk/freenet/src/freenet/pluginmanager/PluginManager.java	2007-10-28 18:33:01 UTC (rev 15623)
+++ trunk/freenet/src/freenet/pluginmanager/PluginManager.java	2007-10-28 18:44:08 UTC (rev 15624)
@@ -4,10 +4,14 @@
 package freenet.pluginmanager;
 
 import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.net.JarURLConnection;
+import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URL;
 import java.net.URLClassLoader;
@@ -17,7 +21,12 @@
 import java.util.Iterator;
 import java.util.Set;
 import java.util.Vector;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarException;
 import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.zip.ZipException;
 
 import freenet.config.InvalidConfigValueException;
 import freenet.config.SubConfig;
@@ -27,10 +36,13 @@
 import freenet.node.Ticker;
 import freenet.node.useralerts.SimpleUserAlert;
 import freenet.node.useralerts.UserAlert;
+import freenet.support.JarClassLoader;
 import freenet.support.Logger;
 import freenet.support.URIPreEncoder;
 import freenet.support.api.HTTPRequest;
 import freenet.support.api.StringArrCallback;
+import freenet.support.io.Closer;
+import freenet.support.io.StreamCopier;
 
 public class PluginManager {
 
@@ -45,7 +57,11 @@
 	 */
 
 	private final HashMap toadletList;
-	private final Vector/*<PluginInfoWrapper>*/ pluginWrappers;
+
+	/* All currently starting plugins. */
+	private final Set/* <PluginProgress> */startingPlugins = new HashSet/* <PluginProgress> */();
+
+	private final Vector/* <PluginInfoWrapper> */pluginWrappers;
 	private PluginRespirator pluginRespirator = null;
 	final Node node;
 	private final NodeClientCore core;
@@ -67,6 +83,7 @@
 			public String[] get() {
 				return getConfigLoadString();
 			}
+
 			public void set(String[] val) throws InvalidConfigValueException {
 				//if(storeDir.equals(new File(val))) return;
 				// FIXME
@@ -76,9 +93,13 @@
 
 		String fns[] = pmconfig.getStringArr("loadplugin");
 		if (fns != null) {
-			for (int i = 0 ; i < fns.length ; i++) {
-				//System.err.println("Load: " + StringArrOption.decode(fns[i]));
-				startPlugin(fns[i], false);
+			for (int i = 0; i < fns.length; i++) {
+				String name = fns[i];
+				boolean refresh = name.endsWith("*");
+				if (refresh) {
+					name = name.substring(0, name.length() - 1);
+				}
+				startPlugin(name, refresh, false);
 			}
 		}
 
@@ -98,8 +119,10 @@
 
 			Vector v = new Vector();
 
-			while(it.hasNext())
-				v.add(((PluginInfoWrapper)it.next()).getFilename());
+			while (it.hasNext()) {
+				PluginInfoWrapper pluginInfoWrapper = (PluginInfoWrapper) it.next();
+				v.add(pluginInfoWrapper.getFilename() + (pluginInfoWrapper.isAutoRefresh() ? "*" : ""));
+			}
 
 			return (String[]) v.toArray(new String[v.size()]);
 		}catch (NullPointerException e){
@@ -108,35 +131,63 @@
 		}
 	}
 
-	public void startPlugin(String filename, boolean store) {
+	/**
+	 * Returns a set of all currently starting plugins.
+	 * 
+	 * @return All currently starting plugins
+	 */
+	public Set/* <PluginProgess> */getStartingPlugins() {
+		synchronized (startingPlugins) {
+			return new HashSet/* <PluginProgress> */(startingPlugins);
+		}
+	}
+
+	public void startPlugin(final String filename, final boolean refresh, final boolean store) {
 		if (filename.trim().length() == 0)
 			return;
-		Logger.normal(this, "Loading plugin: " + filename);
-		FredPlugin plug;
-		try {
-			plug = LoadPlugin(filename);
-			PluginInfoWrapper pi = PluginHandler.startPlugin(this, filename, plug, pluginRespirator);
-			synchronized (pluginWrappers) {
-				pluginWrappers.add(pi);
+		final PluginProgress pluginProgress = new PluginProgress(filename);
+		synchronized (startingPlugins) {
+			startingPlugins.add(pluginProgress);
+		}
+		node.executor.execute(new Runnable() {
+
+			public void run() {
+				Logger.normal(this, "Loading plugin: " + filename);
+				FredPlugin plug;
+				try {
+					plug = loadPlugin(filename, refresh);
+					pluginProgress.setProgress(PluginProgress.STARTING);
+					PluginInfoWrapper pi = PluginHandler.startPlugin(PluginManager.this, filename, plug, pluginRespirator, refresh);
+					synchronized (pluginWrappers) {
+						pluginWrappers.add(pi);
+					}
+					Logger.normal(this, "Plugin loaded: " + filename);
+				} catch (PluginNotFoundException e) {
+					Logger.normal(this, "Loading plugin failed (" + filename + ')', e);
+					String message = e.getMessage();
+					core.alerts.register(new SimpleUserAlert(true, l10n("pluginLoadingFailedTitle"), l10n("pluginLoadingFailedWithMessage", new String[] { "name", "message" }, new String[] { filename, message }), UserAlert.ERROR));
+				} catch (UnsupportedClassVersionError e) {
+					Logger.error(this, "Could not load plugin " + filename + " : " + e, e);
+					System.err.println("Could not load plugin " + filename + " : " + e);
+					e.printStackTrace();
+					String jvmVersion = System.getProperty("java.vm.version");
+					if (jvmVersion.startsWith("1.4.") || jvmVersion.equals("1.4")) {
+						System.err.println("Plugin " + filename + " appears to require a later JVM");
+						Logger.error(this, "Plugin " + filename + " appears to require a later JVM");
+						core.alerts.register(new SimpleUserAlert(true, l10n("pluginReqNewerJVMTitle", "name", filename), l10n("pluginReqNewerJVM", "name", filename), UserAlert.ERROR));
+					}
+				} finally {
+					synchronized (startingPlugins) {
+						startingPlugins.remove(pluginProgress);
+					}
+				}
+				/* try not to destroy the config. */
+				synchronized (this) {
+					if (store)
+						core.storeConfig();
+				}
 			}
-			Logger.normal(this, "Plugin loaded: " + filename);
-		} catch (PluginNotFoundException e) {
-			Logger.normal(this, "Loading plugin failed (" + filename + ')', e);
-		} catch (UnsupportedClassVersionError e) {
-			Logger.error(this, "Could not load plugin "+filename+" : "+e, e);
-			System.err.println("Could not load plugin "+filename+" : "+e);
-			e.printStackTrace();
-			String jvmVersion = System.getProperty("java.vm.version");
-			if(jvmVersion.startsWith("1.4.") || jvmVersion.equals("1.4")) {
-				System.err.println("Plugin "+filename+" appears to require a later JVM");
-				Logger.error(this, "Plugin "+filename+" appears to require a later JVM");
-				core.alerts.register(new SimpleUserAlert(true, 
-						l10n("pluginReqNewerJVMTitle", "name", filename),
-						l10n("pluginReqNewerJVM", "name", filename),
-						UserAlert.ERROR));
-			}
-		}
-		if(store) core.storeConfig();
+		}, "Plugin Starter");
 	}
 
 	void register(FredPlugin plug, PluginInfoWrapper pi) {
@@ -153,10 +204,38 @@
 		}
 	}
 
+	/**
+	 * Returns the translation of the given key, prefixed by the short name of
+	 * the current class.
+	 * 
+	 * @param key
+	 *            The key to fetch
+	 * @return The translation
+	 */
+	private String l10n(String key) {
+		return L10n.getString("PluginManager." + key);
+	}
+
 	private String l10n(String key, String pattern, String value) {
 		return L10n.getString("PluginManager."+key, pattern, value);
 	}
 
+	/**
+	 * Returns the translation of the given key, replacing each occurence of
+	 * <code>${<em>pattern</em>}</code> with <code>value</code>.
+	 * 
+	 * @param key
+	 *            The key to fetch
+	 * @param patterns
+	 *            The patterns to replace
+	 * @param values
+	 *            The values to substitute
+	 * @return The translation
+	 */
+	private String l10n(String key, String[] patterns, String[] values) {
+		return L10n.getString("PluginManager." + key, patterns, values);
+	}
+
 	private void registerToadlet(FredPlugin pl){
 		//toadletList.put(e.getStackTrace()[1].getClass().toString(), pl);
 		synchronized (toadletList) {
@@ -313,180 +392,253 @@
 	}
 
 	/**
-	 * Method to load a plugin from the given path and return is as an object.
-	 * Will accept filename to be of one of the following forms:
-	 * "classname" to load a class from the current classpath
-	 * "classame at file:/path/to/jarfile.jar" to load class from an other jarfile.
-	 *
-	 * @param filename 	The filename to load from
-	 * @return			An instanciated object of the plugin
-	 * @throws PluginNotFoundException	If anything goes wrong.
+	 * Tries to load a plugin from the given name. If the name only contains the
+	 * name of a plugin and the plugin should not be refreshed on startup it is
+	 * loaded from the plugin directory, if found, otherwise it's refresh from
+	 * the project server. If the name contains a complete url and the short
+	 * file already exists in the plugin directory and the plugin should not be
+	 * refreshed, it's loaded from the plugin directory, otherwise it's
+	 * retrieved from the remote server.
+	 * 
+	 * @param name
+	 *            The specification of the plugin
+	 * @param refresh
+	 *            Whether the file should be refreshed on startup
+	 * @return An instanciated object of the plugin
+	 * @throws PluginNotFoundException
+	 *             If anything goes wrong.
 	 */
-	private FredPlugin LoadPlugin(String origFilename)
-	throws PluginNotFoundException {
-		logMINOR = Logger.shouldLog(Logger.MINOR, this);
-		Class cls = null;
-		for (int tries = 0; (tries <= 5) && (cls == null); tries++) {
-			String filename = origFilename;
-			if (filename.endsWith("*")) {
-				filename = "*@http://downloads.freenetproject.org/alpha/plugins/"
-						+ filename.substring(filename.lastIndexOf(".") + 1,
-								filename.length() - 1) + ".jar.url";
-				if (logMINOR)
-					Logger.minor(this, "Rewritten to " + filename);
+	private FredPlugin loadPlugin(String name, boolean refresh) throws PluginNotFoundException {
+		/* check if name contains a URL. */
+		URL pluginUrl = null;
+		try {
+			pluginUrl = new URL(name);
+		} catch (MalformedURLException mue1) {
+		}
+		if (pluginUrl == null) {
+			try {
+				pluginUrl = new URL("http://downloads.freenetproject.org/alpha/plugins/" + name + ".jar.url");
+			} catch (MalformedURLException mue1) {
+				Logger.error(this, "could not build plugin url for " + name, mue1);
+				throw new PluginNotFoundException("could not build plugin url for " + name, mue1);
 			}
+		}
+
+		/* check for plugin directory. */
+		File pluginDirectory = new File("plugins");
+		if ((pluginDirectory.exists() && !pluginDirectory.isDirectory()) || (!pluginDirectory.exists() && !pluginDirectory.mkdirs())) {
+			Logger.error(this, "could not create plugin directory");
+			throw new PluginNotFoundException("could not create plugin directory");
+		}
+
+		/* get plugin filename. */
+		String completeFilename = pluginUrl.getPath();
+		String filename = completeFilename.substring(completeFilename.lastIndexOf('/') + 1);
+		File pluginFile = new File(pluginDirectory, filename);
+
+		/* check if file needs to be downloaded. */
+		if (logMINOR) {
+			Logger.minor(this, "plugin file " + pluginFile.getAbsolutePath() + " exists: " + pluginFile.exists());
+		}
+		if (refresh || !pluginFile.exists()) {
+			File tempPluginFile = null;
+			OutputStream pluginOutputStream = null;
+			URLConnection urlConnection = null;
+			InputStream pluginInputStream = null;
 			try {
-				BufferedReader in = null;
-				InputStream is = null;
-				if ((filename.indexOf("@") >= 0)) {
-					boolean assumeURLRedirect = true;
-					// Open from external file
-					try {
-						String realURL = null;
-						String realClass = null;
+				tempPluginFile = File.createTempFile("plugin-", ".jar", pluginDirectory);
+				pluginOutputStream = new FileOutputStream(tempPluginFile);
+				urlConnection = pluginUrl.openConnection();
+				urlConnection.setUseCaches(false);
+				urlConnection.setReadTimeout(0);
+				urlConnection.setAllowUserInteraction(false);
+				urlConnection.setConnectTimeout(180 * 1000);
+				urlConnection.connect();
+				pluginInputStream = urlConnection.getInputStream();
+				byte[] buffer = new byte[1024];
+				int read;
+				while ((read = pluginInputStream.read(buffer)) != -1) {
+					System.out.println("read " + read + " bytes");
+					pluginOutputStream.write(buffer, 0, read);
+				}
+			} catch (IOException ioe1) {
+				Logger.error(this, "could not load plugin", ioe1);
+				if (tempPluginFile != null) {
+					tempPluginFile.delete();
+				}
+				throw new PluginNotFoundException("could not load plugin: " + ioe1.getMessage(), ioe1);
+			} finally {
+				Closer.close(pluginOutputStream);
+				Closer.close(pluginInputStream);
+			}
+			/* move temp jar to final jar. */
+			if (pluginFile.exists()) {
+				if (!pluginFile.delete()) {
+					Logger.error(this, "could not remove old plugin file");
+					throw new PluginNotFoundException("could not remove old plugin file");
+				}
+			}
+			if (!tempPluginFile.renameTo(pluginFile)) {
+				Logger.error(this, "could not rename temp file to plugin file");
+				throw new PluginNotFoundException("could not rename temp file to plugin file");
+			}
+		}
 
-						// Load the jar-file
-						String[] parts = filename.split("@");
-						if (parts.length != 2) {
-							throw new PluginNotFoundException(
-							"Could not split at \"@\".");
-						}
-						realClass = parts[0];
-						realURL = parts[1];
-						if (logMINOR)
-							Logger.minor(this, "Class: " + realClass + " URL: "
-									+ realURL);
+		/* now get the manifest file. */
+		JarFile pluginJarFile = null;
+		String pluginMainClassName = null;
+		try {
+			pluginJarFile = new JarFile(pluginFile);
+			Manifest manifest = pluginJarFile.getManifest();
+			if (manifest == null) {
+				Logger.error(this, "could not load manifest from plugin file");
+				throw new PluginNotFoundException("could not load manifest from plugin file");
+			}
+			Attributes pluginMainClassAttributes = manifest.getMainAttributes();
+			if (pluginMainClassAttributes == null) {
+				Logger.error(this, "manifest does not contain Plugin-Main-Class attribute");
+				throw new PluginNotFoundException("manifest does not contain Plugin-Main-Class attribute");
+			}
+			pluginMainClassName = pluginMainClassAttributes.getValue("Plugin-Main-Class");
+		} catch (JarException je1) {
+			Logger.error(this, "could not process jar file", je1);
+			throw new PluginNotFoundException("could not process jar file", je1);
+		} catch (ZipException ze1) {
+			Logger.error(this, "could not process jar file", ze1);
+			throw new PluginNotFoundException("could not process jar file", ze1);
+		} catch (IOException ioe1) {
+			Logger.error(this, "error processing jar file", ioe1);
+			throw new PluginNotFoundException("error procesesing jar file", ioe1);
+		} finally {
+			Closer.close(pluginJarFile);
+		}
 
-						if (filename.endsWith(".url")) {
-							if (!assumeURLRedirect) {
-								// Load the txt-file
-								URL url = new URL(parts[1]);
-								URLConnection uc = url.openConnection();
-								in = new BufferedReader(new InputStreamReader(
-										uc.getInputStream()));
+		try {
+			JarClassLoader jarClassLoader = new JarClassLoader(pluginFile);
+			Class pluginMainClass = jarClassLoader.loadClass(pluginMainClassName);
+			Object object = pluginMainClass.newInstance();
+			if (!(object instanceof FredPlugin)) {
+				Logger.error(this, "plugin main class is not a plugin");
+				throw new PluginNotFoundException("plugin main class is not a plugin");
+			}
+			return (FredPlugin) object;
+		} catch (IOException ioe1) {
+			Logger.error(this, "could not load plugin", ioe1);
+			throw new PluginNotFoundException("could not load plugin", ioe1);
+		} catch (ClassNotFoundException cnfe1) {
+			Logger.error(this, "could not find plugin class", cnfe1);
+			throw new PluginNotFoundException("could not find plugin class", cnfe1);
+		} catch (InstantiationException ie1) {
+			Logger.error(this, "could not instantiate plugin", ie1);
+			throw new PluginNotFoundException("could not instantiate plugin", ie1);
+		} catch (IllegalAccessException iae1) {
+			Logger.error(this, "could not access plugin main class", iae1);
+			throw new PluginNotFoundException("could not access plugin main class", iae1);
+		}
+	}
 
-								realURL = in.readLine();
-								if (realURL == null)
-									throw new PluginNotFoundException(
-											"Initialization error: "
-											+ url
-											+ " isn't a plugin loading url!");
-								realURL = realURL.trim();
-								if (logMINOR)
-									Logger.minor(this, "Loaded new URL: "
-											+ realURL + " from .url file");
-								in.close();
-							}
-							assumeURLRedirect = !assumeURLRedirect;
-						}
+	Ticker getTicker() {
+		return node.getTicker();
+	}
 
-						// Load the class inside file
-						URL[] serverURLs = new URL[] { new URL(realURL) };
-						ClassLoader cl = new URLClassLoader(serverURLs);
+	/**
+	 * Tracks the progress of loading and starting a plugin.
+	 * 
+	 * @author David &lsquo;Bombe&rsquo; Roden &lt;bombe at freenetproject.org&gt;
+	 * @version $Id$
+	 */
+	public static class PluginProgress {
 
-						// Handle automatic fetching of pluginclassname
-						if (realClass.equals("*")) {
+		/** State for downloading. */
+		public static final PluginProgress DOWNLOADING = new PluginProgress();
 
-							// Clean URL
-							URI liburi = URIPreEncoder.encodeURI(realURL);
-							if (logMINOR)
-								Logger.minor(this, "cleaned url: " + realURL
-										+ " -> " + liburi.toString());
-							realURL = liburi.toString();
+		/** State for starting. */
+		public static final PluginProgress STARTING = new PluginProgress();
 
-							URL url = new URL("jar:" + realURL + "!/");
-							JarURLConnection jarConnection = (JarURLConnection) url
-							.openConnection();
-							// Java seems to cache even file: urls...
-							jarConnection.setUseCaches(false);
-							JarFile jf = jarConnection.getJarFile();
-							// URLJarFile jf = new URLJarFile(new File(liburi));
-							// is =
-							// jf.getInputStream(jf.getJarEntry("META-INF/MANIFEST.MF"));
+		/** The starting time. */
+		private long startingTime = System.currentTimeMillis();
 
-							// BufferedReader manifest = new BufferedReader(new
-							// InputStreamReader(cl.getResourceAsStream("/META-INF/MANIFEST.MF")));
+		/** The current state. */
+		private PluginProgress pluginProgress;
 
-							// URL url = new URL(parts[1]);
-							// URLConnection uc =
-							// cl.getResource("/META-INF/MANIFEST.MF").openConnection();
+		/** The name by which the plugin is loaded. */
+		private String name;
 
-							is = jf.getInputStream(jf
-									.getJarEntry("META-INF/MANIFEST.MF"));
-							in = new BufferedReader(new InputStreamReader(is));
-							String line;
-							while ((line = in.readLine()) != null) {
-								// System.err.println(line + "\t\t\t" +
-								// realClass);
-								if (line.startsWith("Plugin-Main-Class: ")) {
-									realClass = line.substring(
-											"Plugin-Main-Class: ".length())
-											.trim();
-									if (logMINOR)
-										Logger.minor(this,
-												"Found plugin main class "
-												+ realClass
-												+ " from manifest");
-								}
-							}
-							// System.err.println("Real classname: " +
-							// realClass);
-						}
+		/**
+		 * Private constructor for state constants.
+		 */
+		private PluginProgress() {
+		}
 
-						cls = cl.loadClass(realClass);
+		/**
+		 * Creates a new progress tracker for a plugin that is loaded by the
+		 * given name.
+		 * 
+		 * @param name
+		 *            The name by which the plugin is loaded
+		 */
+		PluginProgress(String name) {
+			this.name = name;
+			pluginProgress = DOWNLOADING;
+		}
 
-					} finally {
-						try {
-							if (is != null)
-								is.close();
-							if (in != null)
-								in.close();
-						} catch (IOException ioe) {
-						}
-					}
-				} else {
-					// Load class
-					try {
-						cls = Class.forName(filename);
-					} catch (ClassNotFoundException e) {
-						throw new PluginNotFoundException(filename);
-					}
-				}
+		/**
+		 * Returns the number of milliseconds this plugin is already being
+		 * loaded.
+		 * 
+		 * @return The time this plugin is already being loaded (in
+		 *         milliseconds)
+		 */
+		public long getTime() {
+			return System.currentTimeMillis() - startingTime;
+		}
 
-				if (cls == null)
-					throw new PluginNotFoundException("Unknown error");
-			} catch (Exception e) {
-				Logger.normal(this, "Failed to load plugin " + filename + " : "
-						+ e, e);
-				if (tries >= 5)
-					throw new PluginNotFoundException("Initialization error:"
-							+ filename, e);
+		/**
+		 * Returns the name by which the plugin is loaded.
+		 * 
+		 * @return The name by which the plugin is loaded
+		 */
+		public String getName() {
+			return name;
+		}
 
-				try {
-					Thread.sleep(100);
-				} catch (Exception ee) {
-				}
-			}
+		/**
+		 * Returns the current state of the plugin start procedure.
+		 * 
+		 * @return The current state of the plugin
+		 */
+		public PluginProgress getProgress() {
+			return pluginProgress;
 		}
 
-		// Class loaded... Objectize it!
-		Object o = null;
-		try {
-			o = cls.newInstance();
-		} catch (Exception e) {
-			throw new PluginNotFoundException("Could not re-create plugin:"
-					+ origFilename, e);
+		/**
+		 * Sets the current state of the plugin start procedure
+		 * 
+		 * @param pluginProgress
+		 *            The current state
+		 */
+		void setProgress(PluginProgress pluginProgress) {
+			this.pluginProgress = pluginProgress;
 		}
 
-		// See if we have the right type
-		if (!(o instanceof FredPlugin)) {
-			throw new PluginNotFoundException("Not a plugin: " + origFilename);
+		/**
+		 * If this object is one of the constants {@link #DOWNLOADING} or
+		 * {@link #STARTING}, the name of those constants will be returned,
+		 * otherwise a textual representation of the plugin progress is
+		 * returned.
+		 * 
+		 * @return The name of a constant, or the plugin progress
+		 */
+		/* @Override */
+		public String toString() {
+			if (this == DOWNLOADING) {
+				return "downloading";
+			} else if (this == STARTING) {
+				return "starting";
+			}
+			return "PluginProgress[name=" + name + ",startingTime=" + startingTime + ",progress=" + pluginProgress + "]";
 		}
 
-		return (FredPlugin) o;
 	}
 
-	Ticker getTicker() {
-		return node.getTicker();
-	}
 }

Added: trunk/freenet/src/freenet/support/JarClassLoader.java
===================================================================
--- trunk/freenet/src/freenet/support/JarClassLoader.java	                        (rev 0)
+++ trunk/freenet/src/freenet/support/JarClassLoader.java	2007-10-28 18:44:08 UTC (rev 15624)
@@ -0,0 +1,162 @@
+/*
+ * freenet - JarClassLoader.java Copyright © 2007 David Roden
+ * 
+ * This program 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 2 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, write to the Free Software Foundation, Inc., 59 Temple
+ * Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package freenet.support;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import freenet.support.io.StreamCopier;
+
+/**
+ * Class loader that loads classes from a JAR file. The JAR file gets copied
+ * to a temporary location; requests for classes and resources from this class
+ * loader are then satisfied from this local copy.
+ * 
+ * @author <a href="mailto:dr at ina-germany.de">David Roden</a>
+ * @version $Id$
+ */
+public class JarClassLoader extends ClassLoader {
+
+	/** The temporary jar file. */
+	private JarFile tempJarFile;
+
+	/**
+	 * Constructs a new jar class loader that loads classes from the jar file
+	 * with the given name in the local file system.
+	 * 
+	 * @param fileName
+	 *            The name of the jar file
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public JarClassLoader(String fileName) throws IOException {
+		this(new File(fileName));
+	}
+
+	/**
+	 * Constructs a new jar class loader that loads classes from the specified
+	 * URL.
+	 * 
+	 * @param fileUrl
+	 *            The URL to load the jar file from
+	 * @param length
+	 *            The length of the jar file if known, <code>-1</code>
+	 *            otherwise
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public JarClassLoader(URL fileUrl, long length) throws IOException {
+		copyFileToTemp(fileUrl.openStream(), length);
+	}
+
+	/**
+	 * Constructs a new jar class loader that loads classes from the specified
+	 * file.
+	 * 
+	 * @param file
+	 *            The file to load classes from
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public JarClassLoader(File file) throws IOException {
+		tempJarFile = new JarFile(file);
+	}
+
+	/**
+	 * Copies the contents of the input stream (which are supposed to be the
+	 * contents of a jar file) to a temporary location.
+	 * 
+	 * @param inputStream
+	 *            The input stream to read from
+	 * @param length
+	 *            The length of the stream if known, <code>-1</code> if the
+	 *            length is not known
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	private void copyFileToTemp(InputStream inputStream, long length) throws IOException {
+		File tempFile = File.createTempFile("jar-", ".tmp");
+		FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
+		StreamCopier.copy(inputStream, fileOutputStream, length);
+		fileOutputStream.close();
+		tempFile.deleteOnExit();
+		tempJarFile = new JarFile(tempFile);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * This method searches the temporary copy of the jar file for an entry
+	 * that is specified by the given class name.
+	 * 
+	 * @see java.lang.ClassLoader#findClass(java.lang.String)
+	 */
+	protected Class findClass(String name) throws ClassNotFoundException {
+		try {
+			String pathName = transformName(name);
+			JarEntry jarEntry = tempJarFile.getJarEntry(pathName);
+			if (jarEntry != null) {
+				long size = jarEntry.getSize();
+				InputStream jarEntryInputStream = tempJarFile.getInputStream(jarEntry);
+				ByteArrayOutputStream classBytesOutputStream = new ByteArrayOutputStream((int) size);
+				StreamCopier.copy(jarEntryInputStream, classBytesOutputStream, size);
+				classBytesOutputStream.close();
+				byte[] classBytes = classBytesOutputStream.toByteArray();
+				Class clazz = defineClass(name, classBytes, 0, classBytes.length);
+				return clazz;
+			}
+			return null;
+		} catch (IOException e) {
+			throw new ClassNotFoundException(e.getMessage(), e);
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * 
+	 * @see java.lang.ClassLoader#findResource(java.lang.String)
+	 */
+	protected URL findResource(String name) {
+		try {
+			return new URL("jar:" + new File(tempJarFile.getName()).toURL() + "!" + name);
+		} catch (MalformedURLException e) {
+		}
+		return null;
+	}
+
+	/**
+	 * Transforms the class name into a file name that can be used to locate
+	 * an entry in the jar file.
+	 * 
+	 * @param name
+	 *            The name of the class
+	 * @return The path name of the entry in the jar file
+	 */
+	private String transformName(String name) {
+		return name.replace('.', '/') + ".class";
+	}
+
+}


Property changes on: trunk/freenet/src/freenet/support/JarClassLoader.java
___________________________________________________________________
Name: svn:keywords
   + Id

Added: trunk/freenet/src/freenet/support/io/Closer.java
===================================================================
--- trunk/freenet/src/freenet/support/io/Closer.java	                        (rev 0)
+++ trunk/freenet/src/freenet/support/io/Closer.java	2007-10-28 18:44:08 UTC (rev 15624)
@@ -0,0 +1,82 @@
+/*
+ * freenet - Closer.java Copyright © 2007 David Roden
+ * 
+ * This program 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 2 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, write to the Free Software Foundation, Inc., 59 Temple
+ * Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package freenet.support.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.jar.JarFile;
+
+/**
+ * Closes various resources. The resources are checked for being
+ * <code>null</code> before being closed, and every possible execption is
+ * swallowed. That makes this class perfect for use in the finally blocks of
+ * try-catch-finally blocks.
+ * 
+ * @author David &lsquo;Roden&rsquo; &lt;bombe at freenetproject.org&gt;
+ * @version $Id$
+ */
+public class Closer {
+
+	/**
+	 * Closes the given output stream.
+	 * 
+	 * @param outputStream
+	 *            The output stream to close
+	 */
+	public static void close(OutputStream outputStream) {
+		if (outputStream != null) {
+			try {
+				outputStream.close();
+			} catch (IOException ioe1) {
+			}
+		}
+	}
+
+	/**
+	 * Closes the given input stream.
+	 * 
+	 * @param inputStream
+	 *            The input stream to close
+	 */
+	public static void close(InputStream inputStream) {
+		if (inputStream != null) {
+			try {
+				inputStream.close();
+			} catch (IOException ioe1) {
+			}
+		}
+	}
+
+	/**
+	 * Closes the given jar file.
+	 * 
+	 * @param jarFile
+	 *            The jar file to close
+	 */
+	public static void close(JarFile jarFile) {
+		if (jarFile != null) {
+			try {
+				jarFile.close();
+			} catch (IOException e) {
+			}
+		}
+	}
+
+}


Property changes on: trunk/freenet/src/freenet/support/io/Closer.java
___________________________________________________________________
Name: svn:keywords
   + Id

Added: trunk/freenet/src/freenet/support/io/StreamCopier.java
===================================================================
--- trunk/freenet/src/freenet/support/io/StreamCopier.java	                        (rev 0)
+++ trunk/freenet/src/freenet/support/io/StreamCopier.java	2007-10-28 18:44:08 UTC (rev 15624)
@@ -0,0 +1,123 @@
+/*
+ * freenet - StreamCopier.java Copyright © 2007 David Roden
+ * 
+ * This program 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 2 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, write to the Free Software Foundation, Inc., 59 Temple
+ * Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+package freenet.support.io;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Helper class that copies bytes from an {@link InputStream} to an
+ * {@link OutputStream}.
+ * 
+ * @author David &lsquo;Roden&rsquo; &lt;bombe at freenetproject.org&gt;
+ * @version $Id$
+ */
+public class StreamCopier {
+
+	/** Default buffer size is 64k. */
+	private static final int DEFAULT_BUFFER_SIZE = 1 << 16;
+
+	/** The current buffer size. */
+	private static int bufferSize = DEFAULT_BUFFER_SIZE;
+
+	/**
+	 * Sets the buffer size for following transfers.
+	 * 
+	 * @param bufferSize
+	 *            The new buffer size
+	 */
+	public static void setBufferSize(int bufferSize) {
+		StreamCopier.bufferSize = bufferSize;
+	}
+
+	/**
+	 * Copies <code>length</code> bytes from the source input stream to the
+	 * destination output stream. If <code>length</code> is <code>-1</code>
+	 * as much bytes as possible will be copied (i.e. until
+	 * {@link InputStream#read()} returns <code>-1</code> to signal the end of
+	 * the stream).
+	 * 
+	 * @param source
+	 *            The input stream to read from
+	 * @param destination
+	 *            The output stream to write to
+	 * @param length
+	 *            The number of bytes to copy
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static void copy(InputStream source, OutputStream destination, long length) throws IOException {
+		long remaining = length;
+		byte[] buffer = new byte[bufferSize];
+		int read = 0;
+		while ((remaining == -1) || (remaining > 0)) {
+			read = source.read(buffer, 0, ((remaining > bufferSize) || (remaining == -1)) ? bufferSize : (int) remaining);
+			if (read == -1) {
+				if (length == -1) {
+					return;
+				}
+				throw new EOFException("stream reached eof");
+			}
+			destination.write(buffer, 0, read);
+			remaining -= read;
+		}
+	}
+
+	/**
+	 * Copies as much bytes as possible (i.e. until {@link InputStream#read()}
+	 * returns <code>-1</code>) from the source input stream to the
+	 * destination output stream.
+	 * 
+	 * @param source
+	 *            The input stream to read from
+	 * @param destination
+	 *            The output stream to write to
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static void copy(InputStream source, OutputStream destination) throws IOException {
+		copy(source, destination, -1);
+	}
+
+	/**
+	 * Find the length of an input stream. This method will consume the complete
+	 * input stream until its {@link InputStream#read(byte[])} method returns
+	 * <code>-1</code>, thus signalling the end of the stream.
+	 * 
+	 * @param source
+	 *            The input stream to find the length of
+	 * @return The numbe of bytes that can be read from the stream
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 */
+	public static long findLength(InputStream source) throws IOException {
+		long length = 0;
+		byte[] buffer = new byte[bufferSize];
+		int read = 0;
+		while (read > -1) {
+			read = source.read(buffer);
+			if (read != -1) {
+				length += read;
+			}
+		}
+		return length;
+	}
+
+}


Property changes on: trunk/freenet/src/freenet/support/io/StreamCopier.java
___________________________________________________________________
Name: svn:keywords
   + Id




More information about the cvs mailing list