001 /*
002 Copyright (C) 2003 Adam Olsen
003
004 This program is free software; you can redistribute it and/or modify
005 it under the terms of the GNU General Public License as published by
006 the Free Software Foundation; either version 1, or (at your option)
007 any later version.
008
009 This program is distributed in the hope that it will be useful,
010 but WITHOUT ANY WARRANTY; without even the implied warranty of
011 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012 GNU General Public License for more details.
013
014 You should have received a copy of the GNU General Public License
015 along with this program; if not, write to the Free Software
016 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
017 */
018
019 package com.valhalla.pluginmanager;
020
021 import java.io.File;
022 import java.io.FileOutputStream;
023 import java.io.InputStream;
024 import java.util.ArrayList;
025 import java.util.Hashtable;
026 import java.util.Properties;
027 import java.util.jar.JarEntry;
028
029 import com.valhalla.settings.Settings;
030
031 /**
032 * This class supports the loading of custom plugins from jar files. The main
033 * class in the Jar should be named after the jar filename and should be in the
034 * "package" String passed to the PluginLoader package. For example, the plugin
035 * LookAndFeel would be in a jar file called LookAndFeel.jar, and if the package
036 * parameter was com.valhalla.jbother.plugins there would need to be a class
037 * called <code>com.valhalla.jbother.plugins.LookAndFeel</code> in the jar
038 * file
039 *
040 * @author Adam Olsen
041 * @version 1.0
042 */
043 public class PluginLoader extends ClassLoader {
044 private static PluginLoader instance = null;
045
046 private static int pluginAPIVersion = 91214;
047
048 private String pluginDir = null;
049
050 private Hashtable loadedClasses = new Hashtable(); // contains information
051 // about what classes
052 // have
053
054 // already been loaded
055 private static ArrayList availablePlugins = new ArrayList(); // the list of
056 // available
057 // plugins
058
059 private static ArrayList invalidPlugins = new ArrayList();
060
061 private static ArrayList incompatiblePlugins = new ArrayList();
062
063 private static ArrayList installedPlugins = new ArrayList();
064
065 private static Hashtable loadedPlugins = new Hashtable();
066
067 /**
068 * The default constructor
069 *
070 * @param pluginDir
071 * the directory to search for plugins in .jar format
072 */
073 private PluginLoader() {
074 }
075
076 /**
077 * Gets the singleton of this class
078 *
079 * @return the PluginLoader singleton
080 */
081 public static PluginLoader getInstance() {
082 if (instance == null)
083 instance = new PluginLoader();
084 return instance;
085 }
086
087 public static PluginLoader getNewInstance() {
088 instance = new PluginLoader();
089 return instance;
090 }
091
092 /**
093 * Finds which plugin contains the specified native library, extracts it to
094 * a temporary location, and returns the path to that location or null if
095 * the native library could not be found.
096 *
097 * @param the
098 * name of the library
099 * @return the location of the newly created temporary file
100 */
101 public String findLibrary(String lib) {
102 String l = super.findLibrary(lib);
103 if (l != null)
104 return l;
105 PluginJAR jar = getPluginThatContains("native/" + lib);
106 if (jar == null)
107 return null;
108
109 try {
110 JarEntry entry = jar.getJarEntry("native/" + lib);
111
112 int index = lib.lastIndexOf('.');
113 String suffix = lib.substring(index, lib.length());
114 lib = lib.substring(0, index);
115 File temporaryDll = File.createTempFile(lib, suffix);
116
117 InputStream inputStream = jar.getInputStream(entry);
118
119 FileOutputStream outputStream = new FileOutputStream(temporaryDll);
120 byte[] array = new byte[8192];
121 for (int i = inputStream.read(array); i != -1; i = inputStream
122 .read(array)) {
123 outputStream.write(array, 0, i);
124 }
125
126 outputStream.close();
127 temporaryDll.deleteOnExit();
128 com.valhalla.Logger.debug(temporaryDll.getPath());
129 return temporaryDll.getPath();
130 } catch (Exception ex) {
131 com.valhalla.Logger.logException(ex);
132 return null;
133 }
134 }
135
136 /**
137 * @return a list of currently loaded plugins
138 */
139 public Hashtable getLoadedPlugins() {
140 return loadedPlugins;
141 }
142
143 /**
144 * @return a list of available plugins
145 */
146 public ArrayList getAvailablePlugins() {
147 return availablePlugins;
148 }
149
150 /**
151 * @return a list of installed plugins
152 */
153 public ArrayList getInstalledPlugins() {
154 return installedPlugins;
155 }
156
157 /**
158 * @return a list of invalid plugins
159 */
160 public ArrayList getInvalidPlugins() {
161 return invalidPlugins;
162 }
163
164 /**
165 * @return a list of incompatible plugins
166 */
167 public ArrayList getIncompatiblePlugins() {
168 return incompatiblePlugins;
169 }
170
171 /**
172 * Returns the jar file for the specified plugin name
173 *
174 * @param name
175 * the plugin name
176 * @return the jar file corresponding to the name
177 */
178 public PluginJAR getPlugin(String name) {
179 PluginJAR jar = null;
180 for (int i = 0; i < availablePlugins.size(); i++) {
181 PluginJAR temp = (PluginJAR) availablePlugins.get(i);
182 if (temp.getName().equals(name))
183 jar = temp;
184 }
185
186 return jar;
187 }
188
189 /**
190 * Returns the PluginJAR that contains <tt>file</tt>
191 *
192 * @param file
193 * the file to search for
194 * @return the PluginJAR that contains <tt>file</tt>
195 */
196 private PluginJAR getPluginThatContains(String file) {
197 PluginJAR jar = null;
198 for (int i = 0; i < availablePlugins.size(); i++) {
199 PluginJAR temp = (PluginJAR) availablePlugins.get(i);
200 if (temp.contains(file))
201 jar = temp;
202 }
203
204 return jar;
205 }
206
207 /**
208 * Returns the plugin that is represented by location
209 *
210 * @param location
211 * the location of the plugin
212 * @return the PluginJAR representing the specified location
213 */
214 private PluginJAR getPluginFromLocation(String location) {
215 PluginJAR jar = null;
216
217 for (int i = 0; i < availablePlugins.size(); i++) {
218 PluginJAR temp = (PluginJAR) availablePlugins.get(i);
219 if (temp.getLocation().equals(location))
220 jar = temp;
221 }
222
223 return jar;
224 }
225
226 /**
227 * Gets the current plugin API version
228 *
229 * @return the current plugin API version
230 */
231 public static int getAPIVersion() {
232 return pluginAPIVersion;
233 }
234
235 /**
236 * Attempts to load the available plugins
237 */
238 public void loadPlugins() {
239 for (int i = 0; i < availablePlugins.size(); i++) {
240 PluginJAR jar = (PluginJAR) availablePlugins.get(i);
241
242 if (!jar.getLoaded()) {
243 try {
244 jar.loadPlugin();
245 } catch (Exception e) {
246 com.valhalla.Logger.logException(e);
247 }
248 } else {
249 com.valhalla.Logger.debug("Plugin is already loaded.");
250 }
251 }
252 }
253
254 /**
255 * Reads in all the available plugins and the information about them
256 */
257 public void findPlugins(String d) {
258 this.pluginDir = d;
259 String files[] = null;
260 File pluginDir = null;
261
262 installedPlugins.clear();
263 ArrayList newAvailable = new ArrayList();
264 invalidPlugins.clear();
265 incompatiblePlugins.clear();
266
267 try {
268 // open up the plugin directory
269 pluginDir = new File(this.pluginDir);
270 if (!pluginDir.isDirectory())
271 return;
272 files = pluginDir.list();
273 } catch (Exception exception) {
274 return;
275 }
276
277 boolean ignoreIncompatible =
278 Settings.getInstance().getBoolean("ignoreIncompatiblePlugins");
279
280 for (int i = 0; i < files.length; i++) {
281 // if it's a jar file, it might be a plugin
282 if (files[i].endsWith(".jar")) {
283 try {
284 PluginJAR jar = null;
285 String loc = pluginDir.getPath() + File.separatorChar
286 + files[i];
287 jar = getPluginFromLocation(loc);
288
289 if (jar == null || !jar.getLoaded()) {
290 jar = new PluginJAR(pluginDir.getPath()
291 + File.separatorChar + files[i]);
292 }
293
294 Properties props = jar.getProperties();
295 String name = props.getProperty("name");
296
297 if (props == null || props.getProperty("mainClass") == null
298 || name == null)
299 invalidPlugins.add(files[i]);
300 else if (ignoreIncompatible && !checkPlatform (props)) {
301 incompatiblePlugins.add(name);
302 com.valhalla.Logger.debug("Incompatible plugin found: " +
303 name + " (" + files[i] + ")");
304 } else {
305 int version = Integer.parseInt(props
306 .getProperty("APIVersion"));
307 if (version != pluginAPIVersion) {
308 invalidPlugins.add(name);
309 } else {
310 newAvailable.add(jar);
311 }
312
313 installedPlugins.add(props);
314 }
315 } catch (Exception ex) {
316 com.valhalla.Logger.debug("Invalid plugin found: "
317 + files[i]);
318 }
319 }
320 }
321
322 availablePlugins = newAvailable;
323 }
324
325 /**
326 * Gets a resource as a stream
327 *
328 * @param resource
329 * the resource to get
330 * @return the stream
331 */
332 public InputStream getResourceAsStream(String resource) {
333 InputStream stream = null;
334 stream = this.getClass().getClassLoader().getResourceAsStream(resource);
335 if (stream != null)
336 return stream;
337
338 PluginJAR jar = getPluginThatContains(resource);
339
340 if (jar != null) {
341 // find the entry in the Jar file
342 JarEntry entry = jar.getJarEntry(resource);
343
344 stream = jar.getInputStream(entry);
345 } else {
346 com.valhalla.Logger.debug("Stream was null for " + resource);
347 }
348
349 return stream;
350 }
351
352 protected Hashtable getLoadedClasses() {
353 return loadedClasses;
354 }
355
356 /**
357 * Loads the Class out of the plugin file
358 *
359 * @param className
360 * the class name to load
361 * @param resolveIt
362 * true to resolve the class (load dependancies)
363 * @return the Class if it was found
364 * @throws <code>ClassNotFoundException</code> if the class could not be
365 * found in the system or any of the plugin files
366 */
367 public synchronized Class loadClass(String className, boolean resolveIt)
368 throws ClassNotFoundException {
369 Class result = null;
370
371 String origName = className;
372
373 // if the Class was already loaded, return it
374 result = (Class) loadedClasses.get(origName);
375 if (result != null)
376 return result;
377
378 // check to see if it's a system class
379 try {
380 result = this.getClass().getClassLoader().loadClass(className);
381 //result = super.findSystemClass( className );
382
383 return result;
384 } catch (ClassNotFoundException exception) {
385 }
386
387 className = className.replaceAll("\\.", "/");
388 className += ".class";
389
390 // if the class name starts with "java", we will not load it
391 // because of the security risks involved. A class in the package
392 // java. or javax. will be able to access protected members of system
393 // Classes, and this isn't good.
394
395 /* *REMOVED* as official JBother plugins are checked by devel team
396 * anyways...
397 * if (className.startsWith("java"))
398 throw new ClassNotFoundException();
399 */
400 PluginJAR jar = getPluginThatContains(className);
401
402 if (jar == null)
403 throw new ClassNotFoundException();
404
405 try {
406 // find the entry in the Jar file
407 JarEntry entry = jar.getJarEntry(className);
408 InputStream stream = jar.getInputStream(entry);
409
410 // read it into a class
411 int len = (int) entry.getSize();
412 int offset = 0;
413 int success = 0;
414 byte[] contents = new byte[len];
415
416 while (success < len) {
417 len -= success;
418 offset += success;
419 success = stream.read(contents, offset, len);
420 if (success == -1)
421 throw new ClassNotFoundException();
422 }
423
424 stream.read(contents, 0, (int) entry.getSize());
425
426 // actually define the class
427 result = defineClass(origName, contents, 0, contents.length);
428
429 if (resolveIt)
430 resolveClass(result);
431
432 // mark this class as already loaded and put it in the cache
433 loadedClasses.put(origName, result);
434
435 return result;
436 } catch (Exception e) {
437 throw new ClassNotFoundException(className);
438 }
439 }
440
441 /**
442 * Checks whether the current platform (OS name and architecture) is
443 * compatible with the given plugin's platform or not.
444 *
445 * @param pluginProps plugin properties
446 * @return true if the plugin is compatible and false otherwise
447 */
448 static boolean checkPlatform (Properties pluginProps)
449 {
450 // @todo it may be worth to introduce 'os.match' and 'arch.match'
451 // plugin properties to perform more flexible regexp match
452
453 String os = pluginProps.getProperty("os");
454 String arch = pluginProps.getProperty("arch");
455
456 boolean ok = os == null || os.equals("all") ||
457 System.getProperty("os.name").startsWith(os);
458
459 if (ok) {
460 ok = arch == null || arch.equals("all") ||
461 System.getProperty("os.arch").equals(arch);
462 }
463
464 return ok;
465 }
466 }