KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > hudson > PluginWrapper


1 package hudson;
2
3 import hudson.util.IOException2;
4 import org.apache.tools.ant.BuildException;
5 import org.apache.tools.ant.Project;
6 import org.apache.tools.ant.taskdefs.Expand;
7 import org.apache.tools.ant.types.FileSet;
8 import org.kohsuke.stapler.StaplerRequest;
9 import org.kohsuke.stapler.StaplerResponse;
10
11 import java.io.BufferedReader JavaDoc;
12 import java.io.File JavaDoc;
13 import java.io.FileInputStream JavaDoc;
14 import java.io.FileOutputStream JavaDoc;
15 import java.io.FileReader JavaDoc;
16 import java.io.FilenameFilter JavaDoc;
17 import java.io.IOException JavaDoc;
18 import java.io.OutputStream JavaDoc;
19 import java.net.URL JavaDoc;
20 import java.net.URLClassLoader JavaDoc;
21 import java.util.ArrayList JavaDoc;
22 import java.util.List JavaDoc;
23 import java.util.jar.Manifest JavaDoc;
24 import java.util.logging.Logger JavaDoc;
25
26 /**
27  * Represents a Hudson plug-in and associated control information
28  * for Hudson to control {@link Plugin}.
29  *
30  * <p>
31  * A plug-in is packaged into a jar file whose extension is <tt>".hpi"</tt>,
32  * A plugin needs to have a special manifest entry to identify what it is.
33  *
34  * <p>
35  * At the runtime, a plugin has two distinct state axis.
36  * <ol>
37  * <li>Enabled/Disabled. If enabled, Hudson is going to use it
38  * next time Hudson runs. Otherwise the next run will ignore it.
39  * <li>Activated/Deactivated. If activated, that means Hudson is using
40  * the plugin in this session. Otherwise it's not.
41  * </ol>
42  * <p>
43  * For example, an activated but disabled plugin is still running but the next
44  * time it won't.
45  *
46  * @author Kohsuke Kawaguchi
47  */

48 public final class PluginWrapper {
49     /**
50      * Plugin manifest.
51      * Contains description of the plugin.
52      */

53     private final Manifest JavaDoc manifest;
54
55     /**
56      * Loaded plugin instance.
57      * Null if disabled.
58      */

59     private Plugin plugin;
60
61     /**
62      * {@link ClassLoader} for loading classes from this plugin.
63      * Null if disabled.
64      */

65     public final ClassLoader JavaDoc classLoader;
66
67     /**
68      * Base URL for loading static resources from this plugin.
69      * Null if disabled. The static resources are mapped under
70      * <tt>hudson/plugin/SHORTNAME/</tt>.
71      */

72     public final URL JavaDoc baseResourceURL;
73
74     /**
75      * Used to control enable/disable setting of the plugin.
76      * If this file exists, plugin will be disabled.
77      */

78     private final File JavaDoc disableFile;
79
80     /**
81      * Short name of the plugin. The "abc" portion of "abc.hpl".
82      */

83     private final String JavaDoc shortName;
84
85     /**
86      * True if this plugin is activated for this session.
87      * The snapshot of <tt>disableFile.exists()</tt> as of the start up.
88      */

89     private final boolean active;
90
91     private final File JavaDoc archive;
92
93     private final List JavaDoc<Dependency> dependencies = new ArrayList JavaDoc<Dependency>();
94
95     private static final class Dependency {
96         public final String JavaDoc shortName;
97         public final String JavaDoc version;
98
99         public Dependency(String JavaDoc s) {
100             int idx = s.indexOf(':');
101             if(idx==-1)
102                 throw new IllegalArgumentException JavaDoc("Illegal dependency specifier "+s);
103             this.shortName = s.substring(0,idx);
104             this.version = s.substring(idx+1);
105         }
106     }
107
108     /**
109      * @param archive
110      * A .hpi archive file jar file, or a .hpl linked plugin.
111      *
112      * @throws IOException
113      * if an installation of this plugin failed. The caller should
114      * proceed to work with other plugins.
115      */

116     public PluginWrapper(PluginManager owner, File JavaDoc archive) throws IOException JavaDoc {
117         LOGGER.info("Loading plugin: "+archive);
118         this.archive = archive;
119
120         this.shortName = getShortName(archive);
121
122         boolean isLinked = archive.getName().endsWith(".hpl");
123
124         File JavaDoc expandDir = null; // if .hpi, this is the directory where war is expanded
125

126         if(isLinked) {
127             // resolve the .hpl file to the location of the manifest file
128
String JavaDoc firstLine = new BufferedReader JavaDoc(new FileReader JavaDoc(archive)).readLine();
129             if(firstLine.startsWith("Manifest-Version:")) {
130                 // this is the manifest already
131
} else {
132                 // in direction
133
archive = resolve(archive, firstLine);
134             }
135             // then parse manifest
136
FileInputStream JavaDoc in = new FileInputStream JavaDoc(archive);
137             try {
138                 manifest = new Manifest JavaDoc(in);
139             } catch(IOException JavaDoc e) {
140                 throw new IOException2("Failed to load "+archive,e);
141             } finally {
142                 in.close();
143             }
144         } else {
145             expandDir = new File JavaDoc(archive.getParentFile(), shortName);
146             explode(archive,expandDir);
147
148             File JavaDoc manifestFile = new File JavaDoc(expandDir,"META-INF/MANIFEST.MF");
149             if(!manifestFile.exists()) {
150                 throw new IOException JavaDoc("Plugin installation failed. No manifest at "+manifestFile);
151             }
152             FileInputStream JavaDoc fin = new FileInputStream JavaDoc(manifestFile);
153             try {
154                 manifest = new Manifest JavaDoc(fin);
155             } finally {
156                 fin.close();
157             }
158         }
159
160         // TODO: define a mechanism to hide classes
161
// String export = manifest.getMainAttributes().getValue("Export");
162

163         List JavaDoc<URL JavaDoc> paths = new ArrayList JavaDoc<URL JavaDoc>();
164         if(isLinked) {
165             parseClassPath(archive, paths, "Libraries", ",");
166             parseClassPath(archive, paths, "Class-Path", " +"); // backward compatibility
167

168             this.baseResourceURL = resolve(archive,
169                 manifest.getMainAttributes().getValue("Resource-Path")).toURL();
170         } else {
171             File JavaDoc classes = new File JavaDoc(expandDir,"WEB-INF/classes");
172             if(classes.exists())
173                 paths.add(classes.toURL());
174             File JavaDoc lib = new File JavaDoc(expandDir,"WEB-INF/lib");
175             File JavaDoc[] libs = lib.listFiles(JAR_FILTER);
176             if(libs!=null) {
177                 for (File JavaDoc jar : libs)
178                     paths.add(jar.toURL());
179             }
180
181             this.baseResourceURL = expandDir.toURL();
182         }
183         ClassLoader JavaDoc dependencyLoader = new DependencyClassLoader(getClass().getClassLoader(),owner);
184         this.classLoader = new URLClassLoader JavaDoc(paths.toArray(new URL JavaDoc[0]), dependencyLoader);
185
186         disableFile = new File JavaDoc(archive.getPath()+".disabled");
187         if(disableFile.exists()) {
188             LOGGER.info("Plugin is disabled");
189             this.active = false;
190         } else {
191             this.active = true;
192         }
193
194         // compute dependencies
195
String JavaDoc v = manifest.getMainAttributes().getValue("Plugin-Dependencies");
196         if(v!=null) {
197             for(String JavaDoc s : v.split(","))
198                 dependencies.add(new Dependency(s));
199         }
200     }
201
202     /**
203      * Loads the plugin and starts it.
204      *
205      * <p>
206      * This should be done after all the classloaders are constructed for
207      * all the plugins, so that dependencies can be properly loaded by plugins.
208      */

209     /*package*/ void load(PluginManager owner) throws IOException JavaDoc {
210         String JavaDoc className = manifest.getMainAttributes().getValue("Plugin-Class");
211         if(className ==null) {
212             throw new IOException JavaDoc("Plugin installation failed. No 'Plugin-Class' entry in the manifest of "+archive);
213         }
214
215         // make sure dependencies exist
216
for (Dependency d : dependencies) {
217             if(owner.getPlugin(d.shortName)==null)
218                 throw new IOException JavaDoc("Dependency "+d.shortName+" doesn't exist");
219         }
220
221         if(!active)
222             return;
223
224         // override the context classloader so that XStream activity in plugin.start()
225
// will be able to resolve classes in this plugin
226
ClassLoader JavaDoc old = Thread.currentThread().getContextClassLoader();
227         Thread.currentThread().setContextClassLoader(classLoader);
228         try {
229             try {
230                 Class JavaDoc clazz = classLoader.loadClass(className);
231                 Object JavaDoc plugin = clazz.newInstance();
232                 if(!(plugin instanceof Plugin)) {
233                     throw new IOException JavaDoc(className+" doesn't extend from hudson.Plugin");
234                 }
235                 this.plugin = (Plugin)plugin;
236                 this.plugin.wrapper = this;
237             } catch (ClassNotFoundException JavaDoc e) {
238                 throw new IOException2("Unable to load " + className + " from " + archive,e);
239             } catch (IllegalAccessException JavaDoc e) {
240                 throw new IOException2("Unable to create instance of " + className + " from " + archive,e);
241             } catch (InstantiationException JavaDoc e) {
242                 throw new IOException2("Unable to create instance of " + className + " from " + archive,e);
243             }
244
245             // initialize plugin
246
try {
247                 plugin.setServletContext(owner.context);
248                 plugin.start();
249             } catch(Throwable JavaDoc t) {
250                 // gracefully handle any error in plugin.
251
throw new IOException2("Failed to initialize",t);
252             }
253         } finally {
254             Thread.currentThread().setContextClassLoader(old);
255         }
256     }
257
258     private void parseClassPath(File JavaDoc archive, List JavaDoc<URL JavaDoc> paths, String JavaDoc attributeName, String JavaDoc separator) throws IOException JavaDoc {
259         String JavaDoc classPath = manifest.getMainAttributes().getValue(attributeName);
260         if(classPath==null) return; // attribute not found
261
for (String JavaDoc s : classPath.split(separator)) {
262             File JavaDoc file = resolve(archive, s);
263             if(file.getName().contains("*")) {
264                 // handle wildcard
265
FileSet fs = new FileSet();
266                 File JavaDoc dir = file.getParentFile();
267                 fs.setDir(dir);
268                 fs.setIncludes(file.getName());
269                 for( String JavaDoc included : fs.getDirectoryScanner(new Project()).getIncludedFiles() ) {
270                     paths.add(new File JavaDoc(dir,included).toURL());
271                 }
272             } else {
273                 if(!file.exists())
274                     throw new IOException JavaDoc("No such file: "+file);
275                 paths.add(file.toURL());
276             }
277         }
278     }
279
280     private static File JavaDoc resolve(File JavaDoc base, String JavaDoc relative) {
281         File JavaDoc rel = new File JavaDoc(relative);
282         if(rel.isAbsolute())
283             return rel;
284         else
285             return new File JavaDoc(base.getParentFile(),relative);
286     }
287
288     /**
289      * Returns the URL of the index page jelly script.
290      */

291     public URL JavaDoc getIndexPage() {
292         return classLoader.getResource("index.jelly");
293     }
294
295     /**
296      * Returns the short name suitable for URL.
297      */

298     public String JavaDoc getShortName() {
299         return shortName;
300     }
301
302     /**
303      * Gets the instance of {@link Plugin} contributed by this plugin.
304      */

305     public Plugin getPlugin() {
306         return plugin;
307     }
308
309     @Override JavaDoc
310     public String JavaDoc toString() {
311         return "Plugin:" + getShortName();
312     }
313
314     /**
315      * Returns a one-line descriptive name of this plugin.
316      */

317     public String JavaDoc getLongName() {
318         String JavaDoc name = manifest.getMainAttributes().getValue("Long-PluginName");
319         if(name!=null) return name;
320         return shortName;
321     }
322
323     /**
324      * Returns the version number of this plugin
325      */

326     public String JavaDoc getVersion() {
327         String JavaDoc v = manifest.getMainAttributes().getValue("Plugin-Version");
328         if(v!=null) return v;
329
330         // plugins generated before maven-hpi-plugin 1.3 should still have this attribute
331
v = manifest.getMainAttributes().getValue("Implementation-Version");
332         if(v!=null) return v;
333
334         return "???";
335     }
336
337     /**
338      * Gets the "abc" portion from "abc.ext".
339      */

340     private static String JavaDoc getShortName(File JavaDoc archive) {
341         String JavaDoc n = archive.getName();
342         int idx = n.lastIndexOf('.');
343         if(idx>=0)
344             n = n.substring(0,idx);
345         return n;
346     }
347
348     /**
349      * Terminates the plugin.
350      */

351     void stop() {
352         LOGGER.info("Stopping "+shortName);
353         try {
354             plugin.stop();
355         } catch(Throwable JavaDoc t) {
356             System.err.println("Failed to shut down "+shortName);
357             System.err.println(t);
358         }
359     }
360
361     /**
362      * Enables this plugin next time Hudson runs.
363      */

364     public void enable() throws IOException JavaDoc {
365         if(!disableFile.delete())
366             throw new IOException JavaDoc("Failed to delete "+disableFile);
367     }
368
369     /**
370      * Disables this plugin next time Hudson runs.
371      */

372     public void disable() throws IOException JavaDoc {
373         // creates an empty file
374
OutputStream JavaDoc os = new FileOutputStream JavaDoc(disableFile);
375         os.close();
376     }
377
378     /**
379      * Returns true if this plugin is enabled for this session.
380      */

381     public boolean isActive() {
382         return active;
383     }
384
385     /**
386      * If true, the plugin is going to be activated next time
387      * Hudson runs.
388      */

389     public boolean isEnabled() {
390         return !disableFile.exists();
391     }
392
393     /**
394      * Explodes the plugin into a directory, if necessary.
395      */

396     private void explode(File JavaDoc archive, File JavaDoc destDir) throws IOException JavaDoc {
397         if(!destDir.exists())
398             destDir.mkdirs();
399
400         // timestamp check
401
File JavaDoc explodeTime = new File JavaDoc(destDir,".timestamp");
402         if(explodeTime.exists() && explodeTime.lastModified()>archive.lastModified())
403             return; // no need to expand
404

405         LOGGER.info("Extracting "+archive);
406
407         // delete the contents so that old files won't interfere with new files
408
Util.deleteContentsRecursive(destDir);
409
410         try {
411             Expand e = new Expand();
412             e.setProject(new Project());
413             e.setTaskType("unzip");
414             e.setSrc(archive);
415             e.setDest(destDir);
416             e.execute();
417         } catch (BuildException x) {
418             IOException JavaDoc ioe = new IOException JavaDoc("Failed to expand " + archive);
419             ioe.initCause(x);
420             throw ioe;
421         }
422
423         Util.touch(explodeTime);
424     }
425
426
427 //
428
//
429
// Action methods
430
//
431
//
432
public void doMakeEnabled(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc {
433         enable();
434         rsp.setStatus(200);
435     }
436     public void doMakeDisabled(StaplerRequest req, StaplerResponse rsp) throws IOException JavaDoc {
437         disable();
438         rsp.setStatus(200);
439     }
440
441
442     private static final Logger JavaDoc LOGGER = Logger.getLogger(PluginWrapper.class.getName());
443
444     /**
445      * Filter for jar files.
446      */

447     private static final FilenameFilter JavaDoc JAR_FILTER = new FilenameFilter JavaDoc() {
448         public boolean accept(File JavaDoc dir,String JavaDoc name) {
449             return name.endsWith(".jar");
450         }
451     };
452
453     /**
454      * Used to load classes from dependency plugins.
455      */

456     final class DependencyClassLoader extends ClassLoader JavaDoc {
457         private final PluginManager manager;
458
459         public DependencyClassLoader(ClassLoader JavaDoc parent, PluginManager manager) {
460             super(parent);
461             this.manager = manager;
462         }
463
464         protected Class JavaDoc<?> findClass(String JavaDoc name) throws ClassNotFoundException JavaDoc {
465             for (Dependency dep : dependencies) {
466                 PluginWrapper p = manager.getPlugin(dep.shortName);
467                 if(p!=null)
468                     try {
469                         return p.classLoader.loadClass(name);
470                     } catch (ClassNotFoundException JavaDoc _) {
471                         // try next
472
}
473             }
474
475             throw new ClassNotFoundException JavaDoc(name);
476         }
477
478         // TODO: delegate resources? watch out for diamond dependencies
479
}
480 }
481
Popular Tags